iOS

A Beginner’s Guide to Automated UI Testing in iOS


You probably have heard about automated tests before. People talk about it a lot these days, especially when the topic is about software quality.

They say that if you don’t write any tests for your project, you’re in big trouble. It may not affect you at the moment. But in the long run, it would become a huge technical debt.

That’s true.

Project with no tests is impossibile to maintain when it gets big and there are multiple developers involved. As you change something in the code, things start to break. You don’t even know that it breaks until your boss comes to your desk and starts yelling. You know that feeling, right?

So, it’s better to know a thing or two about testing so that you can improve your project quality and also improve yourself as a software engineer.

There are 2 types of automated tests in iOS:

  • Unit test:
    • test a specific case in a class.
    • make sure that the class works independently on its own.
  • UI test:
    • is also called Integration test.
    • test user interactions with the app.
    • make sure that all classes fit well together.

Both are equally important.

If you only write unit tests and completely ignore UI tests, you’ll be trapped in this situation:

unittest-integrationtest

As you can see, each window works fine on its own. But when put together, bad things happen. You can see how mad that man is. 😉

UI tests are simple. Even simpler than unit tests.

Today we’re gonna learn to write some very basic UI tests and yet still cover a fully-functional app from start to finish.

What app will we be working on?

It’s a simple note-taking app with the following features:

  • Login with username and password.
  • View list of notes.
  • Create new note.
  • Update existing note.
  • Delete a note.

Here’s a gif that runs through all functionalities of the app:

note-app

Look like a piece of cake, right?

However, our job today is not about how to build such app. What we’re gonna do instead is learning to write automated UI tests for it, for all screens and functionalities. That’s the exciting part.

But what does automated UI tests look like?

Let’s have a look at this video:

As soon as the test suite is launched, it simulates all kinds of user interactions:

  • filling in a text field.
  • tapping a button.
  • swiping around.

It travels through each screen and test all scenarios in sequence:

  • at Login screen, it tests scenario when:
    • entering correct username and password.
    • password field is empty.
    • username field is empty.
    • entering incorrect username or password.
  • at Home screen, it tests scenario when:
    • adding a new note.
    • deleting notes.
    • editting an existing note.

I don’t know about you but I have to admit that it looks funny.

Firing up the UI tests and watch it flies through everything is a fun experience. So next time when someone asks you to show your app, just run the UI tests. He’ll be amazed and impressed at the same time. 😜

Why should we write automated UI tests?

There are some key benefits here:

  • Avoid regression bugs and minimize time spent on manual testing:

    Actually this is the benefit of automated tests in general.

    When you make some changes in the code, the tests are there to make sure that you don’t break anything (regression). And that your app still works as expected.

    If there is no tests, it’s dangerous to make even a slightest change. One has to be very careful and do a lot of manual testing to gain back his confidence.

    If there is no tests, we will need the whole QC team to sit together trying to break the app and looking for bugs. Now we don’t anymore. UI tests will take care of that pretty nicely.

    I’m not saying that we should totally rely on automated tests. There is always something that tests can’t cover. We still need to test the app manually ourselves. However, the time we spend on it is kept to a minimum.

  • Helps test view controllers:

    Given the design of UIKit framework where everything is tightly coupled (the window, the view, the controller, the app lifecycle), that makes it very hard to write unit tests for view controllers even for the simplest scenario.

    It’s still possible but we would have to mock and stub things around, simulate events and stuffs, which turns out to be a ton of work and doesn’t prove useful at all.

    Why fight the system when we can ride with it?

    UI test puts us in the role of a user. A user doesn’t care about any logic underneath. All he wants to do is tapping buttons and expect to see something appear on the screen. That’s it.

  • Helps to document your code:

    Look at this:

This is a UI test for scenario when user tries to login with wrong credentials (username or password).

Actually you don’t need me to tell you what that is. It’s obvious.

You’ll quickly understand what the app can do just by reading the tests. It’s the best documentation.

  • Provides a visual run-through of your app:

    As I mentioned earlier, this is simply fun. It’s a nice thing to have and you’re gonna love it.

    Seriously, you’re gonna love it for sure.

Alright! I hope that gets you a little bit more excited about UI testings. Let’s dive deeper.

Understand UI testing

A typical UI test might look like this (in pseudo code):

As you can see, UI test is composed of actions that a normal user can take. It’s like a step-by-step guide with some expectations along the way. The common format is:

For example:

Another example:

In UI tests, we care about user interactions much more than the underlying code structure. We focus heavily on what a user can do and what he’ll see on the screen. All the background stuffs can be ignored.

How do we express UI test in Swift?

We’re gonna need a framework called KIF. It offers a whole lot of APIs to deal with UI interactions. For example:

Fill in “some random text” into a text field.

Tap a button.

You may wonder: What is this accessibility label thing?

Actually it’s a way to address UI components on the screen.

Or frankly, accessibility label is a name you give for each UIView to distinguish among them.

When you say you wanna tap a button, you have to tell the framework which button to tap. Therefore, you assign it an accessibility label (let’s say “my button”) and then tap it:

Here’s a couple more examples:

Expect a view (with accessibility label of “my view”) to appear on the screen:

Expect a view (with accessibility label of “my view”) to disappear from the screen:

Get a reference to a view with accessibility label of “my view”:

Swipe left on view with accessibility label of “my view”:

For full list of supported UI interactions in KIF, you can read here.

How do we assign accessibility label for UIView?

There are 2 ways that we can do:

  1. Using Storyboard:
  • Open Storyboard.
  • Click on a view you wanna assign accessbility label.
  • Choose the Identity Inspector tab.
al-storyboard-1
  • Scroll down to the Accessibility section.
  • Input the accessbility label you want into the Label field. (“Login – Username” in this case)
al-storyboard-2

For UITableView and UICollectionView, there won’t be an Accessibility section available. I have no idea why Apple does that. However, we can workaround anyway:

al-tableview

So basically what we do is we assign a key path that matches the accessibilityLabel property of UITableView. Then at runtime, it will read value from key path and set its property accordingly.

Another thing to note is that: for UIButton or UILabel, it has a default accessibility label which is equal to its text property. Let’s say you have a button with the text “click me”, then its accessibility label is also “click me”. You don’t need to set it again.

  1. Using code:

If you have a text field that is a reference from the Storyboard:

Then you can:

Although it looks simpler when using code, it is still recommended to set your accessibility label directly from the Storyboard if possible. The best code is no code at all.

Prepare the project

First, download the project here. Run it. Make sure it compiles fine.

Play around with the app to get a sense of what we’re gonna do next.

Editor’s note: If you can’t compile the project, please delete the SimpleNoteTakingApp.xcworkspace folder and run “pod install”.

How to setup KIF for UI testing?

Step 1: Import KIF and Nimble using cocoapods.

Add pod 'KIF' and pod 'Nimble' to your Podfile. Remember to put them in the test target.

Nimble is a framework to help you better express your expectation in test. I once wrote an article about it here.

Open Terminal and run:

Step 2: Create KIF helper for Swift

Create a KIF+Extensions.swift file in your test target.

Step 3: Create a briding header

Create a new temporary Objective-C file in your test target. You can name it to anything.

create-objc-file

Xcode will ask whether you also want to add a briding header. Hit Create Bridging Header.

add-bridging-header

Delete the temporary ObjC file we just created above. We don’t need it anymore.

Import KIF from within the bridging header (SimpleNoteTakingApp-Bridging-Header.h).

Step 4: Create our first UI test

Create a new file in the test target and name it to LoginTests.swift.

Please note that:

  • your UI test must subclass from KIFTestCase.
  • test methods must begin with the word test. For example: testA, testB, testLogin, testSomethingElse, ect…

Now let’s run the test to see how it works (Cmd + U).

It should open up the iOS simulator and stop at the login screen. Wait for a couple of seconds. Then fail.

That’s because we haven’t had any view with accessibility label of hello yet. We’re gonna fix that later. But for now, our first UI test is up and running. It’s cool.

Let’s start writing UI tests for our elegent note-taking app

Test the login screen:

There are 4 scenarios in the login screen:

Scenario 1: Empty username and password.

In this case, the user should see an alert telling him that “Username cannot be empty”.

Now before we move on to the nitty gritty, I want you to take some time and design the scenario first: what step will you perform? And what do you expect out of it?

  • First, we would like our username and password fields to be cleared out.
  • Then we tap the Login button.
  • And we expect to see an alert that says “Username cannot be empty”.

Actually there’s a better way to express these steps, using Gherkin format. Here’s the basic structure of a scenario:

In our case, the scenario would become:

Although this is just something we draft on a paper, or directly in our mind, it’s very close to the human language. Everyone should be able to read and understand.

Let’s translate it into Swift.

Open LoginTests.swift and write our first test. Again, test method name must begin with prefix test.

Then we perform the first step: clear out both fields.

Although this clearOutUsernameAndPasswordFields method is not defined yet, you don’t have to worry about it. Just write what we want first. We’ll fix the compile errors later.

Next step is tapping the “Login” button:

Again, this tapButton method is also undefined. We just put it there to structure the test.

Then we do the same thing with the remaining steps:

Now we have the whole scenario written down in Swift. It’s time to fill in the definition for each method.

To clear the text field, we use the KIF method clearTextFromViewWithAccessibilityLabel, which is quite self-explanatory. So the clearOutUsernameAndPasswordFields would be:

The tapButton method:

And the expectToSeeAlert method:

This is the LoginTests.swift at this point:

Now run your test by pressing Cmd + U.

The simulator will pop up and run over your steps auto-magically.

first-ui-test

The test should pass. (since all functionalities are already implemented)

Let’s do a litte refactoring here. Create a new file called LoginSteps.swift and move all step methods there.

Then the LoginTests.swift would look fairly short and sweet.

Scenario 2: Empty password.

Again, we will start with the scenario design first:

Then translate it:

The fillInUsername method is also very straightforward.

Remember to put the step method in LoginSteps.swift instead of LoginTests.swift. Always keep the test clean.

Run the test. Make sure it passes.

Notice that the 2 tests now have a same common step (clearOutUsernameAndPasswordFields). We will move it to the beforeEach method. That’s where you put things you wanna execute first before each test runs.

Now that we’re quite familiar with writing UI tests. Let’s move on more quickly.

Scenario 3: Wrong username or password

The scenario design:

The implementation:

Note that the first step (clearOutUsernameAndPasswordFields) is already in the beforeEach method so we don’t need to call it here anymore.

The fillInWrongPassword method:

Scenario 4: Correct username and password

The scenario design:

The implementation:

The fillInCorrectPassword method:

This is the correct password because I hard-coded it. (a combination of “appcoda” and “correctPassword”) 😜

About the expectToGoToHomeScreen, how do we know if we’ve already transitioned into another screen?

Well, we do it by:

  • expect the UI elements in the login screen to disappear.
  • expect to see UI elements of the Home screen.

Test the home screen:

Create file HomeTests.swift.

And its corresponding HomeSteps.swift.

Now that we have more than 1 test class (LoginTests and HomeTests). There will be common step methods that we’re gonna reuse between them. Let’s create a base class called BaseUITests.swift.

Then make LoginTests and HomeTest inherit from it.

Create another file called CommonSteps.swift. Move all common step methods there:

So whenever you write a step method, be sure to put it into the right place:

  • steps that are widely used among test classes should belong to CommonSteps.swift.
  • steps that are only used in a specific screen should go to its corresponding step file. (Ex: LoginSteps.swift or HomeSteps.swift)

The Home screen is where we create/edit/delete our notes so there’re a lot of database interactions happening.

We don’t wanna put a bunch of test records into our production database every time we run the UI tests. Instead, we will create a test database and use it in our testing environment.

Since I’m using Realm as the database layer. It only takes me 1 line of code to setup a test database:

This will create a Realm database in memory and it only exists for as long as the tests are still running.

Your project may use a different database technology (CoreData, FMDB, SQLite) but the idea is the same. You create a test database file and direct all test records into it. Your main database file is safe.

We’re gonna put this database setup into the beforeAll block so that it is executed only once.

Now we’re ready for the 4 scenarios of the Home screen.

Scenario 1: When there’s no notes, display label “No notes”

Since Home is not the initial screen. We must have a step to go to Home before we can do anything else.

The implementation:

For the haveNoNotes, we have to delete all records from database:

For visitHomeScreen, it’s a little bit tricky because after the previous test finishes, you may not know which screen you’re on. You may be at the Login screen, Home screen, or any other screen.

It’s hard to go anywhere when you have no idea where you are at the moment, right?

However, if we put ourselves at the initial (Login) screen, there is always at least 1 way to reach every screens in the app.

Therefore, the solution is that no matter where you are, go back to the initial screen first, then you can proceed to other screens very easily.

But how to do that?

The first thing to do is grabbing a reference to the root view controller:

Then depends on your app architecture, we will proceed in different ways. In my case, it’s a navigation controller on top of everything. So all I need to do is popping back to first controller in the navigation stack:

With this in place, we’ll put it in the beforeEach method so every time a test gets executed, it has to go back to root first.

After that, it’s recommended to clear out the database to ensure a fresh start for subsequent tests. We’re gonna move the haveNoNotes step into the beforeEach method too.

The visitHomeScreen method:

The expectToSeeLabel method:

The expectNotToSeeNoteList method:

Scenario 2: Create new note.

The scenario design:

There’s little bit of logic here. The create button is only enabled if there’s some text in the title field, otherwise it will be disabled.

The implementation:

For the expectTheCreateButtonToBeDisabled method, we have to:

  • First, get a reference to the Create button.
  • Then assert its property using Nimble. (If you don’t know what Nimble is, read here)

Same with expectTheCreateButtonToBeEnabled:

The fillInNoteTitle and fillInNoteBody are easy. We just need to fill in the field with some texts.

The expectToSeeNoteWithTitle method can be done using the same approach too.

  • Get a reference to the cell.
  • Assert for its property.

The expectNumberOfNotesInListToEqual method:

Scenario 3: Edit a note

The scenario design:

The implementation:

The have3Notes method: we add 3 records to Realm database.

The tapOnNoteAtRow method:

Scenario 4: Delete notes

The scenario design:

The implementation:

The deleteANote method:

Wrap up

UI tests are easy to learn but yield a lot of benefits.

It might take a couple of days to familiarize yourself with KIF and how to work with accessibility labels. But then after that, you’re unstoppable.

Your UI tests are going to cover all user scenarios across the app. Whenever you change something in the code, you’re good to go as long as the tests are still passing.

The only downside I found is that it really take time to run the whole UI test suite as your app grows. For this note taking app, it take me around 80 seconds to run through everything, considering I’m using an old hackintosh with Intel Core i3.

For bigger real-world app, it may take much longer, from a couple of minutes to even hours. But the good news is we can delegate the test running task to another service called Continuous Integration. That deserves another blog post for itself.

You can download the full project (with all UI tests) here.

If you have any questions regarding UI tests, feel free to put a comment down below. I would love to hear more from you guys. Thanks and have a good day.

iOS
Unit Testing in Xcode 7 with Swift
Tutorial
Creating an Immersive User Experience with Haptic Feedback in iOS
SwiftUI
Building Collection Views in SwiftUI with LazyVGrid and LazyHGrid
Shares