What is the proper way to detect if unit tests are running at runtime in Xcode?

11.9k views Asked by At

When I'm running unit tests, I'd like to skip some code (e.g. I don't want [[UIApplication sharedApplication] openURL:..] to run). I'm looking for a runtime check if I'm currently running units tests or not.

I know I have seen code that checks the Objective-C runtime if unit tests are running but am not able to find it anymore.

7

There are 7 answers

1
Undo On

Select the project, and then the test target:

enter image description here

Select Build Settings and choose All and Combined. Type 'preproc' in the search box - you're after Preprocessor Macros.

enter image description here

Add a macro to the Debug configuration called TEST and set it equal to 1:

enter image description here

Then in your code, you can do this:

#ifndef TEST
    [[UIApplication sharedApplication] doEvilThingForTesting];
#endif 

Or if you have code that you want to only run in a test environment:

#ifdef TEST
    [[UIApplication sharedApplication] doAwesomeTestOnlyThing];
#endif 

It's not exactly runtime, but the unit tester compiles the code before it runs the tests IIRC, so it should be the same effect - you're essentially modifying the code right before running the tests.

2
Bryan Chen On

You can use this method from google-toolbox-for-mac

// Returns YES if we are currently being unittested.
+ (BOOL)areWeBeingUnitTested {
  BOOL answer = NO;
  Class testProbeClass;
#if GTM_USING_XCTEST // you may need to change this to reflect which framework are you using
  testProbeClass = NSClassFromString(@"XCTestProbe");
#else
  testProbeClass = NSClassFromString(@"SenTestProbe");
#endif
  if (testProbeClass != Nil) {
    // Doing this little dance so we don't actually have to link
    // SenTestingKit in
    SEL selector = NSSelectorFromString(@"isTesting");
    NSMethodSignature *sig = [testProbeClass methodSignatureForSelector:selector];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
    [invocation setSelector:selector];
    [invocation invokeWithTarget:testProbeClass];
    [invocation getReturnValue:&answer];
  }
  return answer;
}

The reason that NSClassFromString and NSInvocation are used is to allow code compile without linking to xctest or ocunit

0
Jon Reid On

Rather that sprinkling "am I testing?" conditionals throughout production code, I isolate the check to one place: main. There, I check for an alternate application delegate for testing. If it's available, I use it instead of the the regular application delegate. This completely bypasses the regular launch sequence:

int main(int argc, char *argv[])
{
    @autoreleasepool {
        Class appDelegateClass = NSClassFromString(@"TestingAppDelegate");
        if (!appDelegateClass)
            appDelegateClass = [AppDelegate class];
        return UIApplicationMain(argc, argv, nil, NSStringFromClass(appDelegateClass));
    }
}

You can read more about this technique here: How to Easily Switch Your iOS App Delegate for Testing

1
Gavin Bunney On

The easiest (and working in Xcode 7 with XCTest!) way to check is to have a look at the process info for a matching xctest bundle:

static BOOL isRunningTests(void)
{
    NSDictionary* environment = [[NSProcessInfo processInfo] environment];
    NSString* injectBundle = environment[@"XCInjectBundle"];
    return [[injectBundle pathExtension] isEqualToString:@"xctest"];
}

Source: https://www.objc.io/issues/1-view-controllers/testing-view-controllers/#integration-with-xcode

1
david On

I think you can check like this for Xcode 7.3

-(BOOL) isRunningUnitTests
{
    NSDictionary* environment = [ [ NSProcessInfo processInfo ] environment ];
    NSString* theTestConfigPath = environment[ @"XCTestConfigurationFilePath" ];
    return theTestConfigPath != nil;
}
1
cvetomirst On

Just use this:

+ (BOOL)isUnitTestRunning
{
    Class testProbeClass;
    testProbeClass = NSClassFromString(@"XCTestProbe");
    return (testProbeClass != nil);
}
2
Chuck H On

I'm not sure how long this will continue to work, but it works for me right now with Version 9.0 beta 6 (9M214v).

let isTesting = { () -> Bool in
    if let _ = ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] {
        return true
    } else if let testingEnv = ProcessInfo.processInfo.environment["DYLD_INSERT_LIBRARIES"] {
        return testingEnv.contains("libXCTTargetBootstrapInject.dylib")
    } else {
        return false
    }
}()

No build environment or scheme changes are necessary.

It appears that there are two different environment variables in play depending on whether you are running a single test case or the entire test suite. Also, the variable value also differs depending whether or not you are running in simulator or on a real device.