How to unit test button taps on a view controller with XCTest

In the previous article in this series, we verified that our LoginViewController meets certain requirements for displaying text. When those tests pass, we have proof that our LoginViewController has the title “Podcaster” and that the username field’s placeholder text is “Username or email”. While tests like those are certainly useful for offloading some of the mental burden of manual testing, they’re not sufficient. We also need to be able to verify that our view controller behaves the way it should when the user interacts with it. Today we’ll test taps on the login button to make sure the view controller does what we expect.

As discussed in the previous article, you should focus on testing requirements for your app. This gives you confidence that your app meets those requirements, and it relieves you of the manual testing burden and mental overhead associated with it.

We’ll start by testing the following requirement:

When the username field is blank, and the user taps Login, the app should display an error message that says, “Please enter a username or email”

Let’s add a new test to our LoginTests class from the previous article. First, we’ll set the username field to be blank, simulate a tap on the Login button, and verify that the app shows the error message.

func test_login_without_username() {
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let vc = storyboard.instantiateInitialViewController() as! LoginViewController
    let _ = vc.view

    vc.username!.text = ""

    vc.loginButtonTapped()

    XCTAssertFalse(vc.errorLabel!.isHidden)
    XCTAssertEqual("Please enter a username or email", vc.errorLabel!.text!)
}

Just like in the previous article, we instantiate our storyboard and view controller, make sure the view is loaded by calling vc.view, then set up our test scenario. We set the username field’s text to the empty string, then call loginButtonTapped — the @IBAction method our Login button will call when the user taps on it. And finally, we make our assertions.

First, we assert that the errorLabel on our view controller is not hidden — we’re assuming the errorLabel is hidden until it needs to be displayed. We should have a test to prove this assumption, but that’s outside the scope of this article. I’m convinced you can do it yourself by copying and tweaking the test case above.

If you run this test now with ⌘U or Product > Test, you should see it fail. This failure tells us we have a new requirement that our code does not yet meet. Implementing the code to make this test pass should be straightforward, and once you’re finished you can run the tests again to make sure they pass.

Verifying the next requirement will be very similar:

When the username field is not blank, and the password field is blank, and the user taps Login, the app should display an error message that says, “Please enter a password”

Let’s look at how we might translate that into a unit test:

func test_login_with_username_but_no_password() {
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let vc = storyboard.instantiateInitialViewController() as! LoginViewController
    let _ = vc.view

    vc.username!.text = "josh"
    vc.password!.text = ""

    vc.loginButtonTapped()

    XCTAssertFalse(vc.errorLabel!.isHidden)
    XCTAssertEqual("Please enter a password", vc.errorLabel!.text!)
}

When we set up our scenario this time, the username is set to “josh” to match the first part of the requirement that says the username is not blank. Then we set the password to the empty string to match the second part of the requirement, and again simulator a tap on the Login button. Finally, we make the same assertion as before — that the errorLabel is not hidden — before making our assertion that the errorLabel says “Please enter a password”. Again, you can implement this functionality yourself and run the tests to make sure it works as expected.

Conclusion

If you followed along with the previous article and this one, you now have tests that verify several of your app’s requirements, and you can continue to add tests this way until you have a full suite of automated tests that verify your app’s behavior. But even a few tests are valuable — having just these requirements automatically tested means that you don’t have to worry about these features breaking in the future. You can be confident that as long as your tests pass, your code still works as it should. This gives you the freedom to move faster as you develop new features, knowing old ones won’t break. Your test suite also gives you the ability to refactor with confidence so you can eliminate those code smells that happen as your code grows and evolves.

This article is part of a series on unit testing in Swift — enter your name and email in the boxes below to get the rest so you can break the cycle of manually testing everything.