95

One annoying thing when running tests in Xcode 6.1 is that the entire app has to run and launch its storyboard and root view controller. In my app this runs some server calls that fetches API data. However, I don't want the app to do this when running its tests.

With preprocessor macros gone, what's the best for my project to be aware that it was launched running tests and not an ordinary launch? I run them normally with command + U and on a bot.

Pseudocode:

// Appdelegate.swift
if runningTests() {
   return
} else {
   // do ordinary api calls
}
shim
  • 7,170
  • 10
  • 62
  • 95
bogen
  • 8,549
  • 8
  • 44
  • 82
  • "the entire app has to run and launch its storyboard and root view controller" is that correct? I haven't tested it but it doesn't seem right to me. Hmm... – Fogmeister Dec 16 '14 at 09:14
  • Yes, the application did finish launching is run as well as viewdidload for the root view controller – bogen Dec 16 '14 at 09:15
  • Ah, just tested. Didn't think that was the case lol. What is it about this that is causing a problem in your tests? Maybe there is another way around it? – Fogmeister Dec 16 '14 at 09:17
  • I need to just let the app know its run with tests in mind, so a flag like the old preprocessor macros would work, but they are not supported in swift. – bogen Dec 16 '14 at 09:27
  • Yeah, but **why** do you "need" to do that? What is it that is making you think you need to do that? – Fogmeister Dec 16 '14 at 09:33
  • See also potential duplicate: http://stackoverflow.com/questions/18134670/dont-launch-simulator-when-running-unittests – kortina Jun 06 '15 at 01:24
  • If you assume the premise, there's lots of good solutions here. However, the premise of this question is (at least for some cases) itself questionable. Having a process that's under test behave differently in testing than in production can mess with the validity of your tests — and not necessarily [maliciously](https://en.wikipedia.org/wiki/Volkswagen_emissions_scandal). How do you ensure that you never have code being tested that, say, depends on global state set in an `if runningTests` block? – rickster Nov 10 '16 at 21:08

16 Answers16

92

Elvind's answer isn't bad if you want to have what used to be called pure "Logic Tests". If you'd still like to run your containing host application yet conditionally execute or not execute code depending on whether tests are run, you can use the following to detect if a test bundle has been injected:

if NSProcessInfo.processInfo().environment["XCTestConfigurationFilePath"] != nil {
     // Code only executes when tests are running
}

I used a conditional compilation flag as described in this answer so that the runtime cost is only incurred in debug builds:

#if DEBUG
    if NSProcessInfo.processInfo().environment["XCTestConfigurationFilePath"] != nil {
        // Code only executes when tests are running
    }
#endif

Edit Swift 3.0

if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil {
    // Code only executes when tests are running
}
Community
  • 1
  • 1
Michael McGuire
  • 3,473
  • 1
  • 26
  • 27
  • 3
    Doesn't work in latest Xcode - Environment variable name seems to have changed – amleszk Jan 19 '16 at 18:01
  • 7
    Not sure exactly when this stopped working, but with Xcode 7.3 I'm now using the `XCTestConfigurationFilePath` environment key instead of `XCInjectBundle`. – ospr Mar 24 '16 at 17:15
  • Thanks @ospr, I've edited the answer to work with Xcode 7.3 – Michael McGuire Aug 10 '16 at 16:12
  • @tkuichooseyou `po ProcessInfo.processInfo.environment` has no key `XCTestConfigurationFilePath`. Can you please share your code? This is checked agains a UITest target – Tal Zion Apr 06 '17 at 06:46
  • @TalZion sorry, didn't realize you meant for a UITest target. Did you try the `NSClassFromString("XCTest")` method below? – tkuichooseyou Apr 07 '17 at 16:07
  • @TalZion For UITest targets, it seems you can set a string with the `XCTestConfigurationFilePath` key to `app.launchEnvironment` in your test case `setUp`. Not sure if there are any unintended side effects. – RishiG Apr 12 '17 at 18:52
  • 1
    Works like a charm in Xcode 9.2 and Xcode 9.3 beta for UNIT Tests – Evils Mar 21 '18 at 00:03
  • Works like a charm in Xcode 12.4 for UNIT Tests – SoftDesigner Apr 09 '21 at 12:45
48

Instead of checking if the tests are running to avoid side-effects, you could run the tests without the host app itself. Go to Project Settings -> select the test target -> General -> Testing -> Host Application -> select 'None'. Just remember to include all files you need to run the tests, as well as libraries normally included by the Host app target.

enter image description here

bogen
  • 8,549
  • 8
  • 44
  • 82
  • 1
    How to include bridging headers for the target after removing host application? – Bhargav Oct 19 '16 at 06:44
  • 1
    Tried this one, it broke my tests until I got to this answer that proposes to do just the opposite: https://stackoverflow.com/a/30939244/1602270 – eagle.dan.1349 Mar 02 '18 at 12:56
  • if I do that, I can test cloudKit (for example) so the solution for me is to detect in applicationDidFinishLaunching if i'm testing, and if YES, then return without allocating the main classes of the app. – user1105951 Jun 11 '19 at 09:27
47

I use this in application:didFinishLaunchingWithOptions:

// Return if this is a unit test
if let _ = NSClassFromString("XCTest") {
    return true
}
Jesse
  • 1,507
  • 11
  • 15
35

Other, in my opinion simpler way:

You edit your scheme to pass a boolean value as launch argument to your app. Like this:

Set launch arguments in Xcode

All launch arguments are automatically added to your NSUserDefaults.

You can now get the BOOL like:

BOOL test = [[NSUserDefaults standardUserDefaults] boolForKey:@"isTest"];
iCaramba
  • 2,455
  • 14
  • 30
  • 2
    It seems that this is the cleanest way that I've found so far. We don't have to add all app files to the test target and we don't have to rely on some weird solution checking for `"XCTestConfigurationFilePath"` or `NSClassFromString("XCTest")`. I've implemented this solution in in Swift with a global function ```func isRunningTests() -> Bool { return UserDefaults.standard.bool(forKey: "isRunningTests") }``` – Kevin Hirsch Oct 25 '17 at 06:53
  • In my opinion the best answer to this question if it is too late to remove the host application with reasonable effort. – ANGOmarcello May 18 '21 at 10:20
22

I believe it's completely legitimate to want to know if you're running inside a test or not. There are numerous reasons why that can be helpful. For example, in running tests, I return early from application-did/will-finish-launching methods in the App Delegate, making the tests start faster for code not germane to my unit test. Yet, I can't go pure "logic" test, for a host of other reasons.

I used to use the excellent technique described by @Michael McGuire above. However, I noticed that stopped working for me around Xcode 6.4/iOS8.4.1 (perhaps it broke sooner).

Namely, I don't see the XCInjectBundle anymore when running a test inside a test target for a framework of mine. That is, I'm running inside a test target that tests a framework.

So, utilizing the approach @Fogmeister suggests, each of my test schemes now sets an environment variable that I can check for.

enter image description here

Then, here's some code I have on a class called APPSTargetConfiguration that can answer this simple question for me.

static NSNumber *__isRunningTests;

+ (BOOL)isRunningTests;
{
    if (!__isRunningTests) {
        NSDictionary *environment = [[NSProcessInfo processInfo] environment];
        NSString *isRunningTestsValue = environment[@"APPS_IS_RUNNING_TEST"];
        __isRunningTests = @([isRunningTestsValue isEqualToString:@"YES"]);
    }

    return [__isRunningTests boolValue];
}

The one caveat with this approach is that if you run a test from your main app scheme, as XCTest will let you do, (that is, not selecting one of your test schemes), you won't get this environment variable set.

idStar
  • 10,074
  • 9
  • 52
  • 57
  • 5
    instead of adding it in 'Run', wouldn't it be more helpful if we add it in 'Test' for all the schemes? This way, isRunningTests will work in all the schemes. – Vishal Singh Jun 20 '16 at 07:13
  • @VishalSingh Yes, I believe that is cleaner. Did you give that approach a try? Let us know if it worked just as well for you. – idStar Jun 20 '16 at 13:40
  • Security alert: I'd wrap this in a big #if DEBUG clause, otherwise you're letting "testing" backdoors into production – Oded Ben Dov Mar 15 '21 at 13:53
20
var isRunningTests: Bool {
    return ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil
}

Usage

if isRunningTests {
    return "lena.bmp"
}
return "facebook_profile_photo.bmp"
neoneye
  • 44,507
  • 23
  • 155
  • 143
8

Combined approach of @Jessy and @Michael McGuire

(As accepted answer will not help you while developing a framework)

So here is the code:

#if DEBUG
        if (NSClassFromString(@"XCTest") == nil) {
            // Your code that shouldn't run under tests
        }
#else
        // unconditional Release version
#endif
m8labs
  • 3,530
  • 2
  • 28
  • 28
8

This is the swift way to do it.

extension Thread {
  var isRunningXCTest: Bool {
    for key in self.threadDictionary.allKeys {
      guard let keyAsString = key as? String else {
        continue
      }

      if keyAsString.split(separator: ".").contains("xctest") {
        return true
      }
    }
    return false
  }
}

And this is how you use it:

if Thread.current.isRunningXCTest {
  // test code goes here
} else {
  // other code goes here
}

Here is the full article: https://medium.com/@theinkedengineer/check-if-app-is-running-unit-tests-the-swift-way-b51fbfd07989

Firas Safa
  • 131
  • 1
  • 7
  • 2
    That for loop can be Swiftilized like this: `return threadDictionary.allKeys.anyMatch { ($0 as? String)?.split(separator: ".").contains("xctest") == true }` – Roger Oba Oct 29 '20 at 06:42
5

Here's a way I've been using in Swift 4 / Xcode 9 for our unit tests. It's based on Jesse's answer.

It's not easy to prevent the storyboard being loaded at all, but if you add this at the beginning of didFinishedLaunching then it makes it very clear to your developers what is going on:

func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions:
                 [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    #if DEBUG
    if let _ = NSClassFromString("XCTest") {
        // If we're running tests, don't launch the main storyboard as
        // it's confusing if that is running fetching content whilst the
        // tests are also doing so.
        let viewController = UIViewController()
        let label = UILabel()
        label.text = "Running tests..."
        label.frame = viewController.view.frame
        label.textAlignment = .center
        label.textColor = .white
        viewController.view.addSubview(label)
        self.window!.rootViewController = viewController
        return true
    }
    #endif

(you obviously shouldn't do anything like this for UI tests where you do want the app to startup as normal!)

JosephH
  • 36,107
  • 19
  • 126
  • 149
3

You can pass runtime arguments into the app depending on the scheme here...

enter image description here

But I'd question whether or not it is actually needed.

Fogmeister
  • 70,181
  • 37
  • 189
  • 274
3

The method I had been using stopped working in Xcode 12 beta 1. After trying all of the build based answers to this question, I was inspired by @ODB's answer. Here is a Swift version of a fairly simple solution that works for both Real Devices and Simulators. It should also be fairly "release proof".

Insert in Test setup:

let app = XCUIApplication()
app.launchEnvironment.updateValue("YES", forKey: "UITesting")
app.launch()

Insert in App:

let isTesting: Bool = (ProcessInfo.processInfo.environment["UITesting"] == "YES")

To use it:

    if isTesting {
        // Only if testing
    } else {
        // Only if not testing
    }
Chuck H
  • 4,546
  • 2
  • 24
  • 27
3

First add variable for testing:

enter image description here

and use that in your code:

 if ProcessInfo.processInfo.environment["IS_UNIT_TESTING"] == "1" {
                 // Code only executes when tests are running
 } 
zdravko zdravkin
  • 1,032
  • 11
  • 11
2

Some of these approaches don't work with UITests and if you're basically testing with the app code itself (rather than adding specific code into a UITest target).

I ended up setting an environment variable in the test's setUp method:

XCUIApplication *testApp = [[XCUIApplication alloc] init];

// set launch environment variables
NSDictionary *customEnv = [[NSMutableDictionary alloc] init];
[customEnv setValue:@"YES" forKey:@"APPS_IS_RUNNING_TEST"];
testApp.launchEnvironment = customEnv;
[testApp launch];

Note that this is safe for my testing since I don't currently use any other launchEnvironment values; if you do, you would of course want to copy any existing values first.

Then in my app code, I look for this environment variable if/when I want to exclude some functionality during a test:

BOOL testing = false;
...
if (! testing) {
    NSDictionary *environment = [[NSProcessInfo processInfo] environment];
    NSString *isRunningTestsValue = environment[@"APPS_IS_RUNNING_TEST"];
    testing = [isRunningTestsValue isEqualToString:@"YES"];
}

Note - thanks for RishiG's comment that gave me this idea; I just expanded that to an example.

ODB
  • 306
  • 1
  • 5
0

Worked for me:

Objective-C

[[NSProcessInfo processInfo].environment[@"DYLD_INSERT_LIBRARIES"] containsString:@"libXCTTargetBootstrapInject"]

Swift: ProcessInfo.processInfo.environment["DYLD_INSERT_LIBRARIES"]?.contains("libXCTTargetBootstrapInject") ?? false

0

Apparently in Xcode12 we need to search in the environment key XCTestBundlePath instead of XCTestConfigurationFilePath if you are using the new XCTestPlan

MZapatae
  • 9
  • 2
0

I'm using this in my SwiftUI Scene.body (Xcode 12.5):

if UserDefaults.standard.value(forKey: "XCTIDEConnectionTimeout") == nil {
  // not unit testing
} else {
  // unit testing
}
Matthew
  • 958
  • 9
  • 19