How to let the app know if its running Unit tests in a pure Swift project?

依然范特西╮ 提交于 2019-12-20 08:23:22

问题


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, whats the best for my project to be aware that it was launched running tests and not an ordinary launch? I run them normally with CMD+U and on a bot.

Pseudo code would be:

// Appdelegate.swift

if runningTests() {
   return
} else {
   // do ordinary api calls
}

回答1:


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.




回答2:


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
}



回答3:


I use this in application:didFinishLaunchingWithOptions:

// Return if this is a unit test
if let _ = NSClassFromString("XCTest") {
    return true
}



回答4:


Other, in my opinion simpler way:

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

All launch arguments are automatically added to your NSUserDefaults.

You can now get the BOOL like:

BOOL test = [[NSUserDefaults standardUserDefaults] boolForKey:@"isTest"];



回答5:


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.

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.




回答6:


var isRunningTests: Bool {
    return ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil
}

Usage

if isRunningTests {
    return "lena.bmp"
}
return "facebook_profile_photo.bmp"



回答7:


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!)




回答8:


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

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




回答9:


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



回答10:


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.




回答11:


Worked for me:

Objective-C

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

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



来源:https://stackoverflow.com/questions/27500940/how-to-let-the-app-know-if-its-running-unit-tests-in-a-pure-swift-project

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!