Readable and maintainable UITests

I think the biggest problem in adding UITests to your iOS solution is that it feels like a lot of extra work. Not only creating the test but also maintaining them feels like that. If you did create some UITests the chance is big that you started that by recording your actions and then probably cleaned up the generated code. And? Did it still have quite some identifier strings in it? And did it felt like you were repeating yourself often?

After cleaning up your code will probably still look something like this:

XCUIApplication().tables.cells.staticTexts[“Due Today”].tap()
XCUIApplication().tables.cells.staticTexts[“Details”][2].tap()

Wouldn’t it be nice if you could use some strongly typed and less verbose code like:

HomeScreen.dueButton.tap()
HomeScreen.detailsButton[2].tap()

Doing that is actually quite easy after you add the boilerplate code that you can find below.

Declaring your elements in an enum

So ideally you would have a container called HomeScreen with items like dueButton and detailsButton. One of the most basic structures for that would be an enum. You do have to be aware that this enum needs to be part of both your UITest project and your application target. So first we start by declaring our UI elements in a couple of enums like this:

Then you need some code to extend that enum with extra functionality. By creating an extension you could make that functionality available to every enum you have.

The boilerplate code

By extending the RawRepresentable protocol you will be able to add that functionality to an enum. Since your UITest project is already all about testing, adding functionality to such an generic protocol is OK. You can also get this code plus some other UITest helper functions by importing the cocoapod UITestHelper. (See also https://github.com/evermeer/UITestHelper)

Here is the code that you need (in your UI test project) to give every enum the functionality that you want:

As you can see it's only a couple of lines of code that will give you this functionality.

But then using an enum like this also means that you have to set the accessibilityIdentifier on your elements to the same value as the rawValue of your enum. You could do that in your UIViewController by adding code like:

dueButton.accessibilityIdentifier = HomeScreen.dueButton.rawValue

This does mean that your enum has to be available both in your UI test project and in your application.

Since you probably have a lot of elements that you want to access from your UITest, you could add the following extension to make that code more compact. I know there are a lot of discussions about when you should use your own operator extension. Whell… It's up to you to decide if you would like to use this:

That code will then be reduced to:

dueButton ~~> HomeScreen.dueButton

Note to self: I guess that setting the identifiers should be possible by using reflection which could reduce all the code necessary to a single line in your UIViewController.

The result

With only a couple of lines of code your unit test code already will look a lot cleaner and easier to maintain. But even then you will probably end up with some repetitive code. For most of these you could create some sort of helper function yourself. The UITestHelper Cocoapod library that is described below could help you to solve the most common of these quickly.

More helper functions

The code above will make your tests more readable and maintainable. But then I do have a couple of other functions that could help clean up your test code. For that see the cocoapod UITestHelper (see also https://github.com/evermeer/UITestHelper).Here is a short overview of the functionality that’s also in there:

  • The function tryLaunch for using an enum to forward parameters from your test to your application plus a retry mechanism for starting up the simulator (because that could fail on a build server)
  • The group function to create a collapsable group in your test output
  • The takeScreenshot function for taking screenshots
  • The functions waitUntilExist and waitUntilExistAssert for waiting a moment until the element is available.
  • The functions or and orAssert for a quick selection of one of the two elements which is available.
  • The ifExist, ifNotExist and ifNotExistWaitUntilExist functions for executing conditional code based on availability.
  • The tapAndType and the setSwitch function to manipulate data elements.

Just to have an overview of how your code will look like, here is a snapshot of some (random) tests: