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 Twitter goes out of business and its site 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 7 introduces a powerful new tool for testing iOS 9 user interfaces. Let’s wrap up the chapter by trying it out.

Recording a UI Test

We currently test the functionality of the Show My Tweets button (if not the button tap itself), but not the Send Tweet button. Let’s see how we can make sure that button still does what it’s supposed to.

In the File Navigator, notice that after the PragmaticTweetsTests group we’ve been working with, Xcode also created a PragmaticTweetsUITests group, with a single file, PragmaticTweetsUITests.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 Send Tweet button. If the Xcode window isn’t covered up, you may notice that code is being written inside the testExample for us. Once the SLComposeViewController view comes up to let us compose a tweet, click the Cancel 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: let​ app = ​XCUIApplication​()
3:  app.buttons[​"Send Tweet"​].tap()
4:  app.navigationBars[​"Twitter"​].buttons[​"Cancel"​].tap()
5: }

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 2 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 3, our recorded code asks the XCUIApplication for an array of all buttons currently on screen, and to find the one called Send Tweet. 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 Send Tweet button.

Line 4 does something similar, just more complicated: it asks for any onscreen navigation bar with the title Twitter, and from that finds a button called Cancel. Again, zero or more than one of either of these would fail.

This is a nifty way to discover our UI at runtime, and with further recording we can discover how to interact with other parts of our app. 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.

We know from writing the original ViewController class in the last chapter that all the handleTweetButtonTapped method does is show the SLComposeViewController. So let’s make that the thing we test: on the last line that was recorded, we don’t need to tap the Cancel button—we just want to make sure it exists.

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. And something that’s true or false is something we can expose to the XCUnit methods from the last section!

Change the name of the method to testSendTweet, and rewrite the method as follows:

 func​ testSendTweet() {
 let​ app = ​XCUIApplication​()
  app.buttons[​"Send Tweet"​].tap()
 XCTAssertTrue​(
  app.navigationBars[​"Twitter"​].buttons[​"Cancel"​].exists)
 }

All we’ve changed from the recording is the last line. Now instead of tapping the Cancel button, we just check that it exists, and we wrap this in a call to XCAssertTrue. Now we have a real test: if the button doesn’t exist, the assert causes the test to fail, and then we need to look to see how this could have broken.

Click the diamond next to testSendTweet to run the test. We see the app run in the Simulator, and, back in Xcode we fail with “XCAssertTrue failed.” Yay, we finally have a failable test case! But, wait, shouldn’t this work? We didn’t really change that much from the recording, after all.

It turns out recording can only take us so far. Look at the code: it taps the Send Tweet button and then immediately looks for the Cancel button on the tweet composer. But remember that when the app is running, it takes a second or so for the tweet composer to slide in. Maybe the UI tester is trying to tap the Cancel button before it’s even there, and that’s why it doesn’t exist yet.

This is why we sometimes need the ability to touch up our recordings as we turn them into tests. If we could just get the test runner to wait for a second or two, everything should be fine.

Luckily, we can do just that. The NSThread class has a sleepForTimeInterval method that stalls execution for given number of seconds. That’s usually a bad idea in an app, since we’d be stalling the app’s execution. But here, the app is running in its own process, and what we’re stalling is the app runner. So add a sleep statement right before testing for the Cancel button, like this:

 func​ testSendTweet() {
 let​ app = ​XCUIApplication​()
  app.buttons[​"Send Tweet"​].tap()
 NSThread​.sleepForTimeInterval(2.0)
 XCTAssertTrue​(
  app.navigationBars[​"Twitter"​].buttons[​"Cancel"​].exists)
 }

Run the test again, and this time we pass. Huzzah! Now we can extend our XCUnit testing skills into the realm of the user interface, so if we accidentally delete something from the storyboard, break a connection, or mess up the event-handling method, tests will be able to detect it.

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

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