User Interface Testing

It’s great that we now have the ability to automatically test our app’s logic. If we inadvertently make a breaking change, or our assumptions get broken (like if our sample podcast feed disappears), then we’ll discover it the next time we run our test suite.

However, one thing we haven’t really exposed to testing is the user interface. If we broke the connection from a button to the method it calls, we would never know, because we test the method, not the button itself.

Testing user interfaces has always been really hard to do, which is why a lot of people don’t do it! The testing culture is much stronger among web developers—where you can always post the same HTTP request and scrape the HTML you get back from a server—than among desktop and mobile developers.

Fortunately, Xcode has a powerful tool for testing user interfaces themselves. Let’s try it out.

Recording a UI Test

We currently test the functionality of the Play/Pause button, but not the button tap itself. Let’s see how we can make sure that it’s still connected and does what it’s supposed to.

In the Project Navigator, notice that after the PragmaticPodcastsTests group we’ve been working with, Xcode also created a PragmaticPodcastsUITests group, with a single file, PragmaticPodcastsUITests.swift. Select this file and notice that it has setUp and tearDown methods like before, although their contents are different from what we saw in the regular unit test files. There’s also an empty testExample method, with a comment to get us started:

 func​ testExample() {
 // Use recording to get started writing UI tests.
 // Use XCTAssert and related functions to verify your tests
 // produce the correct results.
 }

“Use recording”? How do we do that? Notice that at the bottom of the content pane (either above the Debug Area or at the bottom of the window if that’s now showing), we have a circular red button. That, as you might suspect, is the Record button. To see how it works, put the cursor inside the testExample method and start a blank line. Now click the Record button.

images/testing/uitest-record-button.png

This launches the app in the Simulator. After a few seconds, once the app is up and running, tap the Play button. If the Xcode window isn’t covered up, you may notice that code is being written inside the testExample for us. Once the audio starts playing, click the Pause button. A few more lines of code get written for us. This is actually all we need to record for now, so click the Record button again to stop the UI recording. Then stop the Simulator with the Stop button on the top toolbar as usual.

images/testing/uitest-stop-record-button.png

Take a look inside testExample to see what the recorder has written for us:

1: func​ testExample() {
2: XCUIDevice​.shared().orientation = .portrait
3: let​ app = ​XCUIApplication​()
4:  app.buttons[​"Play"​].tap()
5:  app.buttons[​"Pause"​].tap()
6: }

The code here is recognizable as Swift, even if the classes are not. And that makes sense because we’re not writing code to create the UI here; we’re using code to discover what’s on screen at a given time. The XCUIApplication object created on line 3 is a sort of proxy that lets us discover what’s going on in the app. We’ll use this to query for onscreen UI elements.

On line 4, our recorded code asks the XCUIApplication for an array of all buttons currently on screen, and to find the one called Play. The first part of this expression is of type XCUIElement, and it works as a sort of query. If it resolves to exactly one object, we can programmatically tap it. If there are zero or more than one matching buttons, an error occurs and our test fails. So already we have a test that would notice if we accidentally deleted the Play/Pause button or gave it the wrong title.

This is a nifty way to discover our UI at runtime, and if our app had different controls to interact with yet, recording would show us how to access them. We can also write this logic by hand, or clean it up after the recorder is finished: for example, we can access buttons by index rather than by name, if that makes more sense to us.

Writing UI Tests

Still, this isn’t much of a test; there’s no condition that we’re testing to be true or false. If we click the diamond next to testExample or run all the tests with U, this test will pass, because there’s nothing to make it fail. So let’s figure out what we want to test.

Let’s think about our previous unit tests: when we tap the Play button, its title should change to Pause. So that’s something we can test for after performing the tap.

This is pretty easy: the XCUIElement expressions that the recorder creates for us have an exists property, which is true if there is one and only one matching view. So, once we tap, it should then be possible to find exactly one button named Pause. And to make a real test of it, we can just XCTAssert that it exists.

Start a new test method called testUI_WhenPlayTapped_UpdatesTitles, and implement it as follows:

1: func​ testUI_WhenPlayTapped_UpdatesTitles() {
2: let​ app = ​XCUIApplication​()
3:  app.buttons[​"Play"​].tap()
4: XCTAssertTrue​(app.buttons[​"Pause"​].exists)
5: XCTAssertTrue​(app.staticTexts[​"CocoaConf001.m4a"​].exists)
6: }

Line 2 is the same as in the recorded test: it creates a local variable, app, to represent the XCUIApplication app-proxy object. Line 3 is also the same as before, and programmatically taps the Play button for us.

The magic is on line 4. We ask the app to look through its array of buttons for one titled Pause, and then to call the exists method to provide a true/false value for whether we found the button. By wrapping this in XCTAssertTrue, we have a test: if the button isn’t found, the test fails. For good measure, line 5 does a similar assert with expected contents of the title label (notice that we access labels via the staticTexts member of XCTUiApplication).

And if this test method does fail, that means we have to figure out what went wrong—perhaps we broke the action to get the tap, the outlet to access the button, or our nifty KVO code that actually changes the button title. At any rate, if we do break something in the storyboard or the code, we’ll find out about it as soon as we run the UI tests again.

Click the diamond next to testUI_WhenPlayTapped_UpdatesTitles. We see the app run in the Simulator, and back in Xcode we see the green checkmark that means our test passed.

images/testing/ui-test-pass.png

One thing to notice about UI tests is that they really run in the simulated app as-is, as opposed to the unit tests where we loaded a specific scene from the storyboard. That’s actually a good thing. Once we have multiple scenes—a table of podcasts, a table of episodes, and then the player—we would want to tap the same controls that go between those scenes to make sure that those user interactions work as expected.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset