22

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.

Bryan Chen
  • 45,816
  • 18
  • 112
  • 143
Ronald Mannak
  • 446
  • 1
  • 4
  • 11

7 Answers7

17

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

Bryan Chen
  • 45,816
  • 18
  • 112
  • 143
13

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

Jon Reid
  • 20,545
  • 2
  • 64
  • 95
12

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.

Undo
  • 25,519
  • 37
  • 106
  • 129
  • 9
    it works but it is also restricted to code compiled with tests, for instance the target application code is not affected (i.e. your app delegate has the macro not defined). My way to solve this was to duplicate DEBUG configuration and set the macro there as well and then edited my scheme to use this new configuration when testing. – Nicola Ferruzzi Jul 29 '14 at 20:30
9

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.

Chuck H
  • 7,434
  • 4
  • 31
  • 34
  • 1
    FYI, this no longer works using Xcode Version 12.0 beta 1 (12A6159). – Chuck H Jul 05 '20 at 19:22
  • In Xcode 14.2 I see the following keys in environment to indicate the code is running in a unit test: `XCInjectBundleInto`, `XCTestSessionIdentifier` and `XCTestBundlePath` – Jason Moore Jan 18 '23 at 13:33
6

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;
}
david
  • 61
  • 1
  • 2
2

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

Gavin Bunney
  • 1,240
  • 1
  • 12
  • 12
  • In Xcode 14.2, I see `XCInjectBundleInto` (not `XCInjectBundle`). There's also `XCTestSessionIdentifier` which seems a bit clearer for detecting unit tests. – Jason Moore Jan 18 '23 at 13:31
2

Just use this:

+ (BOOL)isUnitTestRunning
{
    Class testProbeClass;
    testProbeClass = NSClassFromString(@"XCTestProbe");
    return (testProbeClass != nil);
}
cvetomirst
  • 31
  • 1
  • 1
    Some additional context or explanation would be helpful to explain why this answer is the best solution. – Toby May 17 '17 at 11:42