Unit Testing in Xcode, does it run the app?
Asked Answered
C

10

47

I'm running into a strange problem that I haven't run into before.

When you do cmd+U to run your Unit Tests (OCUnit for example) does it actually call the main.m, new up the appDelegate and run the app as if your had pressed cmd+R?

I only ask because I'm using CoreData behind this DataLayer. I'm mocking out the DataLayer successfully in my tests, but once I implemented a getAll method that is actually calling CoreData, the app/xcode is throwing an exception about the managed object model can't be nil. Which I understand, but I'm not meaning to actually new up the DataLayer class, and I've put a break point in my mainviewcontroller loadView method where it is calling the DataLayer getAll method. It shouldn't matter with tests because this is a mock object, but it's apparently calling the real instance.

So back to my question, when pressing cmd+U does it also run the app first then run the tests?

Condon answered 30/3, 2013 at 3:38 Comment(0)
C
64

The application is actually run but there is a trick you can use to prevent it from running.

int main(int argc, char* argv[]) {
    int returnValue;

    @autoreleasepool {
        BOOL inTests = (NSClassFromString(@"SenTestCase") != nil
                     || NSClassFromString(@"XCTest") != nil);    

        if (inTests) {
            //use a special empty delegate when we are inside the tests
            returnValue = UIApplicationMain(argc, argv, nil, @"TestsAppDelegate");
        }
        else {
            //use the normal delegate 
            returnValue = UIApplicationMain(argc, argv, nil, @"AppDelegate");
        }
    }

    return returnValue;
}
Christogram answered 31/3, 2013 at 1:15 Comment(8)
Very nice, I've never thought of substituting a different app delegate! I would simplify the test to just BOOL runningTests = NSClassFromString(@"SenTestCase") != nil;Howardhowarth
@JonReid Nice addition. I've never thought of simplifying it that way!Christogram
For XCODE 5 - BOOL inTests = (NSClassFromString(@"XCTest") != nil);Bloom
for me the inTests is always YES doesn't even matter if i run the app with cmd+r or with cmd+uJenifer
@Jenifer That's probably a problem in your project settings. Note that the OCTest/XCTest framework shouldn't be linked in your main target. Make sure it's only in your test target.Christogram
yes you are right, there was a POD that linked the OCTest/XCTest frameworksJenifer
you cant stop it from running. you can prevent the appDelegate from continuing OR switch it but you cant stop the appJd
What if I have logic tests, application tests, and the normal application AppDelegates that I want to switch between? How do I switch between application and logic tests?Harvin
B
20

Here's a variation of Sulthan's answer that uses XCTest, which is the default for test classes generated by XCode 5.


int main(int argc, char * argv[])
{
    @autoreleasepool {
        BOOL runningTests = NSClassFromString(@"XCTestCase") != nil;
        if(!runningTests)
        {
            return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        }
        else
        {
            return UIApplicationMain(argc, argv, nil, @"TestAppDelegate");
        }
    }
}

This goes into main.m, which should be under Supporting Files in a standard project layout.

Then in your tests directory add:

TestAppDelegate.h


#import <Foundation/Foundation.h>

@interface TestAppDelegate : NSObject<UIApplicationDelegate>
@end

TestAppDelegate.m


#import "TestAppDelegate.h"

@implementation TestAppDelegate
@end
Balinese answered 14/12, 2013 at 20:42 Comment(0)
L
13

In Swift, I prefere to bypass a normal execution path inside application: didFinishLaunchingWithOptions:

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    guard normalExecutionPath() else {
        window = nil
        return false
    }

    // regular setup

    return true
}

private func normalExecutionPath() -> Bool {
    return NSClassFromString("XCTestCase") == nil
}

Code inside guard will remove any views created from storyboard.

Locale answered 20/12, 2015 at 17:19 Comment(1)
this works perfectly for me, and it seems the simplest solution!! Thank youKuo
D
5

If you're using Swift (you probably don't have a main.c), you have to do these steps :

1: remove @UIApplicationMain in AppDelegate.swift

2: Create an empty TestingAppDelegate.swift

import UIKit
class TestingAppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
}

3: Create a file called main.swift :

import Foundation
import UIKit

let isRunningTests = NSClassFromString("XCTestCase") != nil

if isRunningTests {
   UIApplicationMain(C_ARGC, C_ARGV, nil, NSStringFromClass(TestingAppDelegate))
} else {
   UIApplicationMain(C_ARGC, C_ARGV, nil, NSStringFromClass(AppDelegate))
}
Dulaney answered 26/6, 2015 at 10:38 Comment(3)
XCode 7 (beta 5) + iOS9 - Use Process.argc, Process.unsafeArgv instead of C_ARGC, C_ARGVJacalynjacamar
Using this approach, will the testing AppDelegate still cause the storyboard to load? And if so, can this be avoided?Performing
Full Decent - Yes it does still load. There is a way around this, but I'd like to find a better one. Duplicate the target that is being tested, lets call it myApp and the duplication myApp copy. Uncheck membership for Main.storyboard for the myApp copy target. Add a new storyboard called Main.storyboard and make it a member of the myApp copy target. Change the myApp test target so that it is testing myApp copy instead of myApp. Now the empty Main.storyboard will be used and no view controllers will be instantiated. I'd like to find an easier way though.Nuriel
E
2

Yes, your test target will have a target dependency to the app target, so the app target will be built when you press Cmd+U or Cmd+Shift+U.

Earthshine answered 30/3, 2013 at 20:23 Comment(0)
I
2

I found another solution to the problem:

int main(int argc, char * argv[])
{
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, ({
            ![NSProcessInfo processInfo].environment[@"XCTestConfigurationFilePath"] ?
            @"AppDelegate" :
            nil;                
        }));
    }
}

From here: http://qualitycoding.org/app-delegate-for-tests/#comment-63984

Impious answered 21/4, 2016 at 7:41 Comment(0)
D
2

Using xCode 7 and xCtool

xctool is capable of executing unit tests without running the app.

To get this working,

1 . Update target settings to run without a host app.

Select your project --> then test target --> Set the host application to none.

enter image description here

2. Install xctool , if you don't have it.

brew install xctool

3. Run the tests using terminal with xctool.

xctool -workspace yourWorkspace.xcworkspace -scheme yourScheme run-tests -sdk iphonesimulator
Denman answered 25/5, 2016 at 9:40 Comment(1)
However, the tests won't be run inside the context of an iOS app and many things won't work. For example keychain and Core Data. If you have UI tests, this won't work at all. It would work for unit test though.Christogram
S
2

You can do that by setting the Host Application to None in your Tests Target.

enter image description here

Summitry answered 23/5, 2018 at 23:49 Comment(0)
C
1

Excellent answers above that suggest dynamically changing the application delegate at run time.

The small modification I make is to detect a unit test run by querying NSProcessInfo. The advantage is that you don't need to have a class that can be detected to see if unit tests are running.

    int main(int argc, char * argv[])
    {
        // Put your App delegate class here.
        const Class appDelegateClass = [ATAppDelegate class];

        NSDictionary *const environmentDictionary =
        [[NSProcessInfo processInfo] environment];

        const BOOL runningUnitTests =
        environmentDictionary[@"XCInjectBundleInto"] != nil;

        NSString *delegateName = 
        runningUnitTests ? nil : NSStringFromClass(appDelegateClass);

        @autoreleasepool {
            return UIApplicationMain(argc, argv, nil, delegateName);
        }
    }

The @"XCInjectBundleInto" property in environmentDictionary is the path to your unit tests bundle and is set up by Xcode.

Cornered answered 6/2, 2017 at 10:7 Comment(0)
M
0

I use the approach of Tomasz Bak plus some code of dwb answer and come up with the following:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{

    BOOL runningTests = NSClassFromString(@"XCTestCase") != nil;
    if (runningTests) {
        self.window.rootViewController = [UIViewController new];
        return true;
    }

    // Your normal code below this
    ....
}
Mufinella answered 26/1, 2016 at 10:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.