Documentation/en-us/TestingApps.md
Setting Up Tests in Your Xcode Project
covers everything you need to know to test any Objective-C or Swift function or class.
In this section, we'll go over a few additional hints for testing
classes like UIViewController subclasses.
You can see a short lightning talk covering most of these topics here (the talk begins at 37'50").
UIViewController Lifecycle EventsNormally, UIKit triggers lifecycle events for your view controller as it's
presented within the app. When testing a UIViewController, however, you'll
need to trigger these yourself. You can do so in one of three ways:
UIViewController.view, which triggers things like UIViewController.viewDidLoad().UIViewController.beginAppearanceTransition() and UIViewController.endAppearanceTransition() to trigger most lifecycle events. Note - as of iOS SDK 13.0, this no longer triggers UIViewController.viewDidAppear().UIViewController.viewDidLoad() or UIViewController.viewWillAppear().// Swift
import Quick
import Nimble
import BananaApp
class BananaViewControllerSpec: QuickSpec {
override class func spec() {
var viewController: BananaViewController!
beforeEach {
viewController = BananaViewController()
}
describe(".viewDidLoad()") {
beforeEach {
// Method #1: Access the view to trigger BananaViewController.viewDidLoad().
let _ = viewController.view
}
it("sets the banana count label to zero") {
// Since the label is only initialized when the view is loaded, this
// would fail if we didn't access the view in the `beforeEach` above.
expect(viewController.bananaCountLabel.text).to(equal("0"))
}
}
describe("the view") {
beforeEach {
// Method #2: Triggers .viewDidLoad() and .viewWillAppear() events.
viewController.beginAppearanceTransition(true, animated: false)
viewController.endAppearanceTransition()
}
// ...
}
describe(".viewWillDisappear()") {
beforeEach {
// Method #3: Directly call the lifecycle event.
viewController.viewWillDisappear(false)
}
// ...
}
}
}
// Objective-C
@import Quick;
@import Nimble;
#import "BananaViewController.h"
QuickSpecBegin(BananaViewControllerSpec)
__block BananaViewController *viewController = nil;
beforeEach(^{
viewController = [[BananaViewController alloc] init];
});
describe(@"-viewDidLoad", ^{
beforeEach(^{
// Method #1: Access the view to trigger -[BananaViewController viewDidLoad].
[viewController view];
});
it(@"sets the banana count label to zero", ^{
// Since the label is only initialized when the view is loaded, this
// would fail if we didn't access the view in the `beforeEach` above.
expect(viewController.bananaCountLabel.text).to(equal(@"0"))
});
});
describe(@"the view", ^{
beforeEach(^{
// Method #2: Triggers .viewDidLoad() and .viewWillAppear() events.
[viewController beginAppearanceTransition:YES animated:NO];
[viewController endAppearanceTransition];
});
// ...
});
describe(@"-viewWillDisappear", ^{
beforeEach(^{
// Method #3: Directly call the lifecycle event.
[viewController viewWillDisappear:NO];
});
// ...
});
QuickSpecEnd
To initialize view controllers defined in a storyboard, you'll need to assign a Storyboard ID to the view controller:
Once you've done so, you can instantiate the view controller from within your tests:
// Swift
var viewController: BananaViewController!
beforeEach {
// 1. Instantiate the storyboard. By default, it's name is "Main.storyboard".
// You'll need to use a different string here if the name of your storyboard is different.
let storyboard = UIStoryboard(name: "Main", bundle: nil)
// 2. Use the storyboard to instantiate the view controller.
viewController =
storyboard.instantiateViewControllerWithIdentifier(
"BananaViewControllerID") as! BananaViewController
}
// Objective-C
__block BananaViewController *viewController = nil;
beforeEach(^{
// 1. Instantiate the storyboard. By default, it's name is "Main.storyboard".
// You'll need to use a different string here if the name of your storyboard is different.
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
// 2. Use the storyboard to instantiate the view controller.
viewController = [storyboard instantiateViewControllerWithIdentifier:@"BananaViewControllerID"];
});
Buttons and other UIKit classes inherit from UIControl, which defines methods
that allow us to send control events, like button taps, programmatically.
To test behavior that occurs when a button is tapped, you can write:
// Swift
describe("the 'more bananas' button") {
it("increments the banana count label when tapped") {
viewController.moreButton.sendActionsForControlEvents(
UIControlEvents.TouchUpInside)
expect(viewController.bananaCountLabel.text).to(equal("1"))
}
}
// Objective-C
describe(@"the 'more bananas' button", ^{
it(@"increments the banana count label when tapped", ^{
[viewController.moreButton sendActionsForControlEvents:UIControlEventTouchUpInside];
expect(viewController.bananaCountLabel.text).to(equal(@"1"));
});
});
Tests sometimes need to wait for apps to complete operations such as animations and network calls that run asynchronously, either on a background queue or on a separate turn of the main run loop. (Aside: note that in most cases, tests should stub network calls and not actually use the network.)
The standard XCTest way of handling asynchronous operations is to use expectations. Quick supports these, but be careful! Do not use self to get an instance of XCTest. This includes creating or waiting for XCTest expectations:
it("makes a network call") {
// 🛑 WRONG: don’t use self.expectation in Quick
let expectation = self.expectation(description: "network call")
URLSession.shared.dataTask(with: URL(string: "https://example.com")!) {
_ in expectation.fulfill()
}.resume()
// 🛑 WRONG: don’t use self.waitForExpectations in Quick
self.waitForExpectations(timeout: 1)
}
Why is this bad? Because when Quick runs your spec() function, it runs it on a dummy instance of XCTest. The real XCTest does not appear until those it closures actually run. In your it closures, self captures that dummy instance. Using this dummy instance to work with expectations is broken in two ways:
waitForExpectations().The solution is to use QuickSpec.current, which returns the currently executing instance of XCTest:
// Swift
it("makes a network call") {
let expectation = QuickSpec.current.expectation(description: "network call")
URLSession.shared.dataTask(with: URL(string: "https://example.com")!) {
_ in expectation.fulfill()
}.resume()
QuickSpec.current.waitForExpectations(timeout: 1)
}
// Objective-C
it(@"makes a network call", ^{
XCTestExpectation *expectation = [QuickSpec.current expectationWithDescription:@"network call"];
NSURLSessionTask *task = [NSURLSession.sharedSession
dataTaskWithURL: [NSURL URLWithString:@"https://example.com"]
completionHandler: ^(NSData *data, NSURLResponse *response, NSError *error) {
[expectation fulfill];
}];
[task resume];
[QuickSpec.current waitForExpectationsWithTimeout:1 handler:NULL];
});
Nimble’s expect(…).toEventually(…) can also help test asynchronous operations:
it("makes a network call") {
var networkCallCompleted = false
URLSession.shared.dataTask(with: URL(string: "https://example.com")!) {
_ in networkCallCompleted = true
}.resume()
expect(networkCallCompleted).toEventually(beTrue())
}
This approach has several drawbacks:
fulfill immediately triggers success whereas toEventually() polls for it.However, toEventually() can lead to simpler test code, and may be a better choice when these concerns do not apply.