/ViewControllerPresentationSpy

Unit test presented view controllers, alerts, and action sheets for iOS

Primary LanguageObjective-COtherNOASSERTION

ViewControllerPresentationSpy

Build Status Carthage compatible CocoaPods Version Twitter Follow

ViewControllerPresentationSpy intercepts presented view controllers, including alerts and actions sheets.

Segues can be captured. For alerts, no actual alerts are presented. This means:

  • 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.

For more discussion, see my blog post How to Test UIAlertControllers and Control Swizzling.

Writing Tests

What do I need to change in production code?

Nothing.

How do I test a presented view controller?

  1. Instantiate a PresentationVerifier before the Act phase of the test.
  2. Invoke the code to create and present your view controller.

Information about the presentation is then available through the PresententationVerifier.

For example, here's a test verifying that one view controller was presented, that its type is correct, and that it has a particular property. sut is the System Under Test in the test fixture.

func test_presentedVC_shouldHaveSpecialSettingHello() {
    let presentationVerifier = PresentationVerifier()

    sut.showVC() // Whatever presents the view controller

    XCTAssertEqual(presentationVerifier.presentedCount, 1, "presented count")
    guard let nextVC = presentationVerifier.presentedViewController as? MyViewController else {
        XCTFail("Expected MyViewController, but was \(presentationVerifier.presentedViewController)")
        return
    }
    XCTAssertEqual(nextVC.specialSetting, "Hello!")
}
- (void) test_presentedVC_shouldHaveSpecialSettingHello {
    QCO PresentationVerifier * presentationVerifier = [[QCO PresentationVerifier alloc] init];

    [sut showVC]; // Whatever presents the view controller

    XCTAssertEqual(presentationVerifier.presentedCount, 1, @"presented count");
    if (![presentationVerifier.presentedViewController isKindOfClass:[MyViewController class]])
    {
        XCTFail(@"Expected MyViewController, but was %@", presentationVerifier.presentedViewController);
        return;
    }
    MyViewController *nextVC = presentationVerifier.presentedViewController;
    XCTAssertEqualObjects(nextVC.specialSetting, @"Hello!");
}

How do I test a segue?

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 sendActions(for: .touchUpInside) on the button.

Segue 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 statue, such as listening to the NotificationCenter. You may need to add special methods outside of deinit 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.

    let window = UIWindow()
    window.rootViewController = sut
    window.isHidden = false

To clean up memory at the end, add this to the beginning of the tearDown() method of the test suite to pump the run loop:

RunLoop.current.run(until: Date())

This ensures that both the presenting view controller and the presented view controller are deallocated at the end of the test case.

How do I test an alert controller?

  1. Instantiate an AlertVerifier before the Act phase of the test.
  2. Invoke the code to create and present your alert or action sheet.

Information about the alert or action sheet is then available through the AlertVerifier.

For example, here's a test verifying the title (and that the alert is presented exactly once). sut is the System Under Test in the test fixture.

func test_showAlert_alertShouldHaveTitle() {
    let alertVerifier = AlertVerifier()

    sut.showAlert() // Whatever triggers the alert

    XCTAssertEqual(alertVerifier.presentedCount, 1, "presented count")
    XCTAssertEqual(alertVerifier.title, "Hello!", "title")
}
- (void)test_showAlert_alertShouldHaveTitle {
    QCOAlertVerifier *alertVerifier = [[QCOAlertVerifier alloc] init];

    [sut showAlert]; // Whatever triggers the alert

    XCTAssertEqual(alertVerifier.presentedCount, 1, @"presented count");
    XCTAssertEqualObjects(alertVerifier.title, @"Hello!", @"title");
}

How can I invoke the closure associated with a UIAlertAction?

Go through the steps above to present your alert or action sheet. Then call executeAction(forButton:) on your AlertVerifier with the button title. For example:

func test_executingActionForOKButton_shouldDoSomething() throws {
    let alertVerifier = AlertVerifier()
    sut.showAlert()
    
    try alertVerifier.executeAction(forButton: "OK")

    // Now assert what you want
}
- (void)test_executingActionForOKButton_shouldDoSomething {
    QCOAlertVerifier *alertVerifier = [[QCOAlertVerifier alloc] init];
    [sut showAlert];

    NSError *error = nil;
    [alertVerifier executeActionForButton:@"OK" returningError:&error];

    XCTAssertNil(error);
    // Now add your own assertions
}

Because this method can throw an exception, declare the Swift test method as throws and call the method with try. For Objective-C, pass in an NSError and check that it's not nil.

How can I invoke the closure passed to present(_:animated:completion:)?

The production code completion handler is captured in the verifier's capturedCompletion property.

How can I test something that's presented using DispatchQueue.main?

Create an expectation in your test case. Fulfill it in the verifier's testCompletion closure. Add a short wait at the start of the Assert phase.

func test_showAlertOnMainDispatchQueue_shouldDoSomething() {
    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
}
func test_presentViewControllerOnMainDispatchQueue_shouldDoSomething() {
    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.

Adding it to your project

CocoaPods

Add the following to your Podfile, changing "MyTests" to the name of your test target:

target 'MyTests' do
  inherit! :search_paths
  pod 'ViewControllerPresentationSpy', '~> 4.0'
end

Carthage

Add the following to your Cartfile:

github "jonreid/ViewControllerPresentationSpy" ~> 4.0

Building It Yourself

Make sure to copy everything from Source/ViewControllerPresentationSpy.