In past articles about test-driven iOS, I have talked about how to resolve some challenges of unit testing an iOS app. This time I’ll talk about higher level tests that validate how the view looks and behaves when users interact with it (UI tests) as well as the app’s API requests and response handling (integration tests).
A feature test validates both the UI and the API calls of an app to document how a feature should work.
Today we’ll look at an example feature test in Swift. I’ll start by explaining the structure and purpose of feature tests, but you can skip to the code if you prefer.
The point of a feature test
We use unit tests to make sure that the individual components of the classes in our app are working. We use integration tests to make sure that a collection of those classes work correctly together. More broadly, we use integration tests to check whether the classes in our app work correctly together with the API of another app that talks to our app. A UI test makes sure that the app responds correctly when users tap buttons, move dials, and otherwise interact with the app.
Our feature test brings the integration and UI tests together to create a test that describes how a feature works. It provides security that the app works as expected, which gives us the freedom to refactor and release with confidence.
Features are like onions
When we start writing a feature in our iOS app, we can begin by imagining how that feature should look and act when we finish writing it. For example, we can say “This feature will allow users to enter a string in a search field. Then, when the user taps the ‘search’ button, our app will call an outside service and display the results of the search.” When our app’s functionality meets that description, the feature is done.
We can do a similar thing with feature tests. Once we finish imagining how a feature should work, we can codify our understanding in a feature test—which will fail, because we have not written that functionality yet. As we write our code, we fulfill the requirements of the feature. When the feature test passes, we know we are done!
One programmer that I paired with recommended starting every feature with a feature test, then jumping all the way in to the innermost layer (say, a service that makes an API call) and writing unit tests and code in there. When those unit tests pass, we commit the change, minus the feature test. Then we move out to the ViewController, do unit tests, write code, commit. We continue this pattern until the feature test passes, and we commit the feature test last. So we start from the outside and go in, but commit from the inside out.
Feature tests in Swift
UI Tests came to XCode in 2015, but the Swift feature test APIs do not have much documentation. In this example, we will look at some of the options in the feature test API. This example serves as a starting point; it does not exhaustively catalog the capabilities of the API. The idea is to get you started so you can continue to explore the APIs according to your needs for testing your app.
An age-old integration test problem…solved by Pact?
When we write integration tests, we usually have to decide whether to integrate with the real API or hook up our tests to a mock. The tradeoff is this: with a mock API, we define the endpoints, so our tests will not fail if the real API goes down or changes. But if we use the real API, say, to test search results, and the data coming down changes, then our assertions will fail even though our integration itself is copacetic.
The Pact Consumer library is designed to resolve this either/or question for Swift and Objective C. Pact verifies the request object that our app is sending and, if that request works against the real API, sends our app a canned response containing data that we define. So, for example, if we are testing a search API, Pact could make sure our request fulfills a contract, hit the api with a request that fits that contract, and upon getting a 200 response send our app a list of search results containing predefined values that we can check in the UI display, et cetera.
I have used Pact in the following feature test example for an iOS app written in Swift.
First, let’s take a look at our feature test code. Then we can go through it by chunks and talk about what it does.
How much of that made sense without any explanation? Ideally, these tests can clearly tell the story of how a feature works so developers can use them to learn about a code base or understand why something is not working.
Now I’ll provide the same feature test, with a lot of comments added in for explanation.
Working with feature tests like this will frequently require some experimentation. I have found it helpful to look into the classes providing the feature test API to figure out which methods might be helpful for my use cases. If feature tests do not appear to be running as expected, it can also be helpful to watch them run in the iOS simulator. All iOS tests run on the simulator, so you can watch the screen to see what might be happening. In some cases a button might not be getting focus because the view hasn’t loaded to tap it, or a static text might not show up because the screen size of the simulator is smaller than the screen size of the simulator that the feature test was designed against. Though the feature testing process has its hangups—especially at this relatively early stage in API development—the feature tests can provide an excellent way to define and track progress while building iOS apps.