ViewControllerPresentationSpy
ViewControllerPresentationSpy has three verifiers that work with either XCTest or Swift Testing.
AlertVerifierto capture alerts and action sheetsPresentationVerifierto capture presented view controllersDismissalVerifierto capture dismissed view controllers
- The workflow doesn't pause for an alert action to be selected.
- Tests are blazing fast.
- You can test things with unit tests instead of UI tests.
Contents
* Writing Tests * What do I need to change in production code? * How do I test an alert controller? * How can I invoke the closure associated with a UIAlertAction? * How do I test a presented view controller? * How do I test a segue? * How do I test dismissing a modal? * How can I invoke the closure passed to present or dismiss? * How can I test something that's presented or dismissed in a Task or using DispatchQueue.main? * Can I see some examples? * How Do I Add ViewControllerPresentationSpy to My Project? * Swift Package Manager * CocoaPods * Prebuilt Framework * Build Your Own * About the Author
Writing Tests
What do I need to change in production code?
Nothing.
How do I test an alert controller?
- Instantiate an
AlertVerifierbefore the Act phase of the test. - Invoke the code to create and present your alert or action sheet.
For example, here's a test verifying:
- That one alert was presented, with animation.
- That the presenting view controller was the System Under Test.
- The alert title.
- The alert message.
- The preferred style of UIAlertController.Style.
- The titles and styles of each action.
sut is the System Under Test in the test fixture. The Swift version uses a handy verify method.
@MainActor
func test_showAlert() throws {
let alertVerifier = AlertVerifier()
sut.showAlert() // Whatever triggers the alert
alertVerifier.verify(
title: "Hello!",
message: "How are you?",
animated: true,
actions: [
.default("OK"),
.cancel("Cancel"),
],
presentingViewController: sut
)
}
``obj-c
{
QCOAlertVerifier *alertVerifier = [[QCOAlertVerifier alloc] init];
[sut showAlert]; // Whatever triggers the alert
XCTAssertEqual(alertVerifier.presentedCount, 1, @"presented count"); XCTAssertEqualObjects(alertVerifier.title, @"Hello!", @"title"); XCTAssertEqualObjects(alertVerifier.message, @"How are you?", @"message"); XCTAssertEqual(alertVerifier.animated, YES, @"animated"); XCTAssertEqual(alertVerifier.preferredStyle, UIAlertController.Style.alert, @"preferred style"); XCTAssertIdentical(alertVerifier.presentingViewController, sut, @"presenting view controller"); XCTAssertEqual(alertVerifier.actions.count, 2, @"actions count); XCTAssertEqualObjects(alertVerifier.actions[0].title, @"OK", @"first action"); XCTAssertEqual(alertVerifier.actions[0].style, UIAlertActionStyleDefault, @"first action"); XCTAssertEqualObjects(alertVerifier.actions[1].title, @"Cancel", @"second action"); XCTAssertEqual(alertVerifier.actions[1].style, UIAlertActionStyleCancel, @"second action"); }
swift @MainActor func test_tappingOK_doesThing() throws { let alertVerifier = AlertVerifier() sut.showAlert() try alertVerifier.executeAction(forButton: "OK")### How can I invoke the closure associated with a UIAlertAction?executeAction (forButton:)Go through the steps above to present your alert or action sheet. Then call
on yourAlertVerifierwith the button title. For example:
// Now assert what you want }
obj-c
- (void)test_tappingOK_doesThing
NSError *error = nil; [alertVerifier executeActionForButton:@"OK" andReturnError:&error];
XCTAssertNil(error); // Now add your own assertions }
swift @MainActor func test_presentedView_hasSpecialSettingHello() throws { let presentationVerifier = PresentationVerifier()Because this method can throw an exception, declare the Swift test method asthrowsand call the method withtry. For Objective-C, pass in an NSError and check that it's not nil.PresentationVerifierHow do I test a presented view controller?
- Instantiate a
before the Act phase of the test.sutInvoke the code to create and present your view controller. Information about the presentation is then available through the PresentationVerifier.For example, here's a test verifying:
- That one view controller was presented, with animation. - That the presenting view controller was the System Under Test. - That the type of the presented view controller is correct. - That the presented view controller has a particular property.
is the System Under Test in the test fixture. The Swift version uses a handyverifymethod.
sut.showVC() // Whatever presents the view controller
let nextVC: MyViewController? = presentationVerifier.verify(animated: true, presentingViewController: sut) XCTAssertEqual(nextVC?.specialSetting, "Hello!") }
obj-c
- (void) test_presentedView_hasSpecialSettingHello
[sut showVC]; // Whatever presents the view controller
XCTAssertEqual(presentationVerifier.presentedCount, 1, @"presented count"); XCTAssertTrue(presentationVerifier.animated, @"animated"); XCTAssertIdentical(presentationVerifier.presentingViewController, sut, @"presenting view controller"); if (![presentationVerifier.presentedViewController isKindOfClass:[MyViewController class]]) { XCTFail(@"Expected MyViewController, but was %@", presentationVerifier.presentedViewController); return; } MyViewController *nextVC = presentationVerifier.presentedViewController; XCTAssertEqualObjects(nextVC.specialSetting, @"Hello!"); }
let window = UIWindow() window.rootViewController = sut window.isHidden = false### How do I test a segue?sendActions(for: .touchUpInside)It depends. First, follow the steps above for testing a presented view controller. Trigger the segue from test code. For example, we can trigger a segue attached to a button by calling
on the button.deinitSegue Type: Present Modally
That's all you need to do. _But you need to be aware of a memory issue:_
Neither the presenting view controller nor the presented view controller will be deallocated during test execution. This can cause problems during test runs if either affects global state, such as listening to the NotificationCenter. You may need to add special methods outside of
that allow tests to clean them up.Segue Type: Show
A "Show" segue (which does push navigation) takes a little more work.
First, install the presenting view controller as the root view controller of a UIWindow. Make this window visible.
RunLoop.current.run(until: Date())To clean up memory at the end, add this to the beginning of thetearDown()method of the test suite to pump the run loop:
swift @MainActor func test_dismissingView() throws { let dismissalVerifier = DismissalVerifier()This ensures that the window is deallocated at the end of the test case. That way, both the view controllers will also cease to exist.DismissalVerifierHow do I test dismissing a modal?
- Instantiate a
before the Act phase of the test.sutInvoke the code to dismiss your modal. Information about the dismissal is then available through the DismissalVerifier.For example, here's a test verifying that a particular view controller was dismissed, with animation.
is the System Under Test in the test fixture. The Swift version uses a handyverifymethod.
sut.dismissVC() // Whatever dismisses the view controller
dismissalVerifier.verify(animated: true, dismissedViewController: sut) }
obj-c
- (void) test_dismissingView
[sut dismissVC]; // Whatever dismisses the view controller
XCTAssertEqual(dismissalVerifier.dismissedCount, 1, @"dismissed count"); XCTAssertTrue(dismissalVerifier.animated, @"animated"); XCTAssertIdentical(dismissalVerifier.dismissedViewController, sut, @"dismissed view controller"); }
swift @MainActor func test_showingAlertOnMainDispatchQueue_doesThing() throws { let alertVerifier = AlertVerifier() let expectation = self.expectation(description: "alert presented") alertVerifier.testCompletion = { expectation.fulfill() } sut.showAlert() waitForExpectations(timeout: 0.001) // Now assert what you want }### How can I invoke the closure passed to present or dismiss?capturedCompletionThe production code completion handler is captured in the verifier's
property.testCompletionHow can I test something that's presented or dismissed in a Task or using DispatchQueue.main?
Create an expectation in your test case. Fulfill it in the verifier's
closure. Add a shortwaitForExpectationsat the start of the Assert phase.
swift
@MainActor
func test_presentViewOnMainDispatchQueue_doesThing() throws {
let presentationVerifier = PresentationVerifier()
let expectation = self.expectation(description: "view controller presented")
presentationVerifier.testCompletion = { expectation.fulfill() }
sut.showVC()
waitForExpectations(timeout: 0.001)
// Now assert what you want
}
### Can I see some examples?
There are sample apps in both Swift and Objective-C. Run them on both phone & pad to see what
they do, then read the
ViewControllerAlertTests
and
ViewControllerPresentationTests.
For Swift Testing, read
ViewControllerAlertSwiftTests
and
ViewControllerPresentationSwiftTests.
How Do I Add ViewControllerPresentationSpy to My Project?
Swift Package Manager
Include a ViewControllerPresentationSpy package in your Package.swift manifest's array of dependencies:
swift
dependencies: [
.package(url: "https://github.com/jonreid/ViewControllerPresentationSpy", .from "7.0.0"),
],
### CocoaPods
Add the following to your Podfile, changing "MyTests" to the name of your test target:
ruby
target 'MyTests' do
inherit! :search_paths
pod 'ViewControllerPresentationSpy', '~> 7.0'
end
### Prebuilt Framework
A prebuilt binary is available on GitHub.
The binary is packaged as ViewControllerPresentationSpy.xcframework, containing these architectures:
- Mac Catalyst
- iOS device
- iOS simulator
- tvOS device
- tvOS simulator
Drag the XCFramework into your project.
Build Your Own
If you want to build ViewControllerPresentationSpy yourself, clone the repo, then
sh
$ cd Source
$ ./MakeDistribution.sh
``
About the Author
Jon Reid is the author of _iOS Unit Testing by Example._ Find more at Quality Coding.