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?
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:
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.
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
}
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()
.
© 2022 - 2024 — McMap. All rights reserved.