iOS UI Testing On an Isolated View
Asked Answered
U

3

42

I'm trying to incorporate UI tests in my iOS project, but one thing that continues to hold me up is the fact that it seems all of the tests you write must start from the beginning of the app and work their way through. For example, if I want to test a view that is behind a login screen, my tests must first run on the login screen, enter a username/password, click login, then go to the view I want to test. Ideally, the tests for the login view and the next one would be completely isolated. Is there a way to do this, or am I missing the philosophy behind UI tests completely?

Unbend answered 17/12, 2015 at 17:38 Comment(1)
I really really really want this to work nicely: app starts clean, the test code decides how to present the VC, and the test code uses Automation to interact with the UI. This is UI Unit Testing. All of my research on this topic is here gist.github.com/fulldecent/529849bc5dd4464bbde2 maybe someone else can pick up the torch.Khorma
F
32

Absolutely!

What you need is a clean application environment in which you can run your tests - a blank slate.

All applications have an application delegate which sets up the initial state of the application and provides a root view controller on launch. For the purposes of testing you don't want that to happen - you need to be able to test in isolation, without all of those things happening. Ideally you want to be able to have the screen undertest and only that screen loaded, and no other state changes happen.

To do so you can create an object just for testing that implements UIApplicationDelegate. You can tell the application to run in "testing mode" and use the testing-specific application delegate using a launch argument.

Objective-C: main.m:

int main(int argc, char * argv[]) {
NSString * const kUITestingLaunchArgument   = @"org.quellish.UITestingEnabled";

    @autoreleasepool {
        if ([[NSUserDefaults standardUserDefaults] valueForKey:kUITestingLaunchArgument] != nil){
            return UIApplicationMain(argc, argv, nil, NSStringFromClass([TestingApplicationDelegate class]));
        } else {
            return UIApplicationMain(argc, argv, nil, NSStringFromClass([ProductionApplicationDelegate class]));
        }
    }
}

Swift: main.swift:

let kUITestingLaunchArgument = "org.quellish.UITestingEnabled"

if (NSUserDefaults.standardUserDefaults().valueForKey(kUITestingLaunchArgument) != nil){
    UIApplicationMain(Process.argc, Process.unsafeArgv, NSStringFromClass(UIApplication), NSStringFromClass(TestingApplicationDelegate))

} else {
    UIApplicationMain(Process.argc, Process.unsafeArgv, NSStringFromClass(UIApplication), NSStringFromClass(AppDelegate))
}

You will have to remove any @UIApplicationMain annotation from your Swift classes.

For "application tests" be sure to set the "Test" action of the scheme in Xcode to provide the launch argument:

Xcode Scheme editor

For UI tests you can set the launch arguments as part of the test:

Objective-C:

XCUIApplication *app = [[XCUIApplication alloc] init];
[app setLaunchArguments:@[@"org.quellish.UITestingEnabled"] ];
[app launch];

Swift:

let app = XCUIApplication()
app.launchArguments = [ "org.quellish.UITestingEnabled" ]
app.launch()

This allows the tests to use an application delegate specificly for testing. This empowers you with a lot of control - you now have a blank slate to work with for testing. The testing application delegate can load a specific storyboard or put in place an empty UIViewController. As part of your UI tests you might instantiate the view controller under test and set it as the keyWindow's root view controller or present it modally. Once it has been added or presented your tests can execute, and when complete remove or dismiss it.

Franconia answered 18/12, 2015 at 1:46 Comment(3)
This solution seems really nice! However, 'NSUserDefaults.standardUserDefaults().valueForKey(kUITestingLaunchArgument) != nil' didn't worked for me. I changed it to NSProcessInfo.processInfo().arguments.contains(kUITestingLaunchArgument) to make it work.Mummy
@FyodorVolchyok I would strongly suggest filing a radar, as user defaults should work. Launch arguments supersede other defaults as they are in the argument domain.Franconia
@Franconia You cannot load a UIStoryboard easily in the test (unless you expose a private var as in your post quellish.tumblr.com/post/135415677047/…). What I do is use additional arguments so the TestAppDelegate knows that UIStoryboard and UIViewController to instantiate.Cascio
K
10

If you don't mind the original UI loading, just jump to the target UI with:

override func setUp() {
    super.setUp()
    continueAfterFailure = false
    XCUIApplication().launch()
    let storyboard = UIStoryboard(name: "MainStoryboard", bundle: NSBundle.mainBundle())
    let controller = storyboard.instantiateViewControllerWithIdentifier("LanguageSelectController")
    UIApplication.sharedApplication().keyWindow?.rootViewController = controller
}

If you do not want the original UI underneath to load then also pass in this from your test:

app.launchArguments.append("skipEntryViewController")

and then in didFinishLaunchingWithOptions, you can check:

if NSProcessInfo.processInfo().arguments.contains("skipEntryViewController") {
    // then do NOT call makeKeyAndVisible
}
Khorma answered 13/1, 2016 at 19:26 Comment(7)
The question specifies that what is needed is a clean and isolated testing, this is not.Capitulate
How is it not meeting that requirement?Khorma
Keeping the same app delegate and the original UI loading may do some work that could interfere.Capitulate
On [UIApplication sharedApplication].keyWindow.rootViewController I´m getting null any idea why ?Alcatraz
When I try to instantiate the storyboard with UIStoryboard(name: "Main", bundle: Bundle.main), the app crashes and the test fails, giving me no feedback. Any ideas?Meagan
@jotaEsse, Because the bundle that is loaded is the testing bundle, not the application bundle. I'm stuck at this point too. Did you ever find a workaround?Senaidasenalda
Unable to find the storyboard even with the right name ??? Do we need to import anything for that ??Olmos
G
-4

Unfortunately, with UI Testing the scenario you describe is not possible.

One approach I take to combat this is to group my tests into "flows" of features. For example, let's say I want to test Feature A, Feature B, and Feature C. I need to be logged in for all three to work.

For each test I don't launch the app, log in, then finally run the actual test. Instead, I launch the app and login once. Then I group my test into three private helper methods, testFeatureA(), testFeatureB(), and testFeatureC().

By creating a single flow the test suite will take a much shorter time to run. The big downside is that if Feature A fails then Feature B will never be tested. This approach should only be used if you care if all of your tests pass or not.

Bonus points for using an XCTest helper with the __LINE__ and __FILE__ parameters defaulted. Then you can pass those into your XCTFail() calls to show the failure line on testFeatureA().

Guria answered 17/12, 2015 at 19:24 Comment(1)
Downvoted as it is in fact possible (see quellish answer)Capitulate

© 2022 - 2024 — McMap. All rights reserved.