Robot Pattern Testing for XCUITest

I recently spoke at iOSDevUK giving an overview of how we test our iOS app at Capital One. The most common follow up question I have been asked is regarding using the Robot Pattern.
I touched on the robot Pattern briefly in the slides in regards to how we make our end-to-end UI tests simple to follow, and have to admit I gave it little extra thought. But clearly this is something others are interested in knowing more about, and there’s very little about the Robot Pattern online, especially for iOS.

The Robot Pattern was designed by Jake Wharton at Square, when testing in Kotlin. As a result much of the information available focusses on Kotlin and Espresso testing. In fact, this is why we settled on the Robot Pattern, as it meant consistency in approach between our Android and iOS testing.
With that in mind, I’d highly recommend Jake’s talk on the Robot Pattern that offers a far better introduction than I will manage here. While Jake’s code may be Kotlin specific, the overall principals at the beginning of the talk apply to any UI.

Why Use the Robot Pattern

There’s three big reasons to use the Robot Pattern when writing XCUI Tests.

  1. Ease of understanding
    We came to XCUITests from Calabash, where our tests were written in Cucumber. Cucumber is close to natural language, meaning anyone, not just engineers fluent in that specific language could read and understand what the tests were testing, quickly and easily, without having to know exactly how the test works. Write the tests in native code, and there’s already a learning curve to knowing whats being tested and what isn’t.

  2. Reuse of code
    By breaking down our tests into steps, each implementation step can be re-used as many times as needed. If your app has a login screen before performing any action in the app, thats a lot of setup for each test. Instead you can just call login() each time, then move on to the more specific areas of your test. If you do need to do something a little different, you can pass parameters into the function.

  3. Isolating implementation details
    Whatever architecture your app uses, your aim is the single responsibility principle. Sticking to this allows you to switch out an object for a new one, with a new implementation, while still keeping the objects core functionality. This allows for easier to maintain, test, and improve code. So why should your tests be different? Jake Wharton describes this as separating the ‘What’ from the ‘How’. Your test should only be concerned with the ‘what’, meaning if your view changes how things appear or happen on screen, you don’t need to change your whole test suite.

Writing an XCUITest

Lets imagine we’re writing UI tests for Apple’s built in Messages app. If you load the app from suspended you’re greeted with a large title ‘Messages’ at the top of the screen, and a button to create a new message. Let’s assume we’re testing tapping on this button, and sending a new message to a user with iMessage. Our XCUITest might look something like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func test_sendNewiMessage() {
let app = XCUIApplication()
app.launch()

app.buttons["new_message"].tap()

let newMessage = app.staticTexts["New Message"]
let predicate = NSPredicate(format: "exists == true")
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: newMessage)
let result = XCTWaiter.wait(for: [expectation], timeout: 5)
XCTAssertEqual(result, .completed)

app.typeText("iMessage Contact")

let newimessage = app.staticTexts["New iMessage"]
let newimessagePredicate = NSPredicate(format: "exists == true")
let newiMessageExpectation = XCTNSPredicateExpectation(predicate: newimessagePredicate, object: newimessage)
let newiMessageResult = XCTWaiter.wait(for: [newiMessageExpectation], timeout: 5)
XCTAssertEqual(newiMessageResult, .completed)

let firstField = app.textFields["messageField"]

firstField.typeText("test iMessage")
app.buttons["send"].tap()

let message = app.staticTexts["test iMessage"]
let messagePredicate = NSPredicate(format: "exists == true")
let messageExpectation = XCTNSPredicateExpectation(predicate: messagePredicate, object: message)
let messageResult = XCTWaiter.wait(for: [messageExpectation], timeout: 5)
XCTAssertEqual(messageResult, .completed)
}

It’s pretty clear there are several issues with this test - theres a lot of duplicated code and it’s difficult to follow what is being tested. But what if we also want to test sending a new message to an SMS contact, we’d have to duplicate this whole test. Then if we make a genuine change to the UI, we’ll have to change both tests.

Creating a Robot

During this test, we’re accessing two screens. The initial list of conversations, the one with the large heading ‘Messages’, then the conversation detail screen that appears once we tap the new message button. We’ll create a base Robot class that contains some common functions like asserting elements exist and tapping on the screen. Each screen will then have its own Robot class that extends Robot. These screen specific Robots contain actions specific to that screen, so our conversations list will contain one high level function to create a new message, our conversations detail has more for this test, as thats where we spend most of our time.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import XCTest

class Robot {
var app = XCUIApplication()

func tap(_ element: XCUIElement, timeout: TimeInterval = 5) {
let expectation = XCTNSPredicateExpectation(predicate: NSPredicate(format: "isHittable == true"), object: element)
guard XCTWaiter.wait(for: [expectation], timeout: timeout) == .completed else {
XCTAssert(false, "Element \(element.label) not hittable")
}
}

func assertExists(_ elements: XCUIElement..., timeout: TimeInterval = 5) {
let expectation = XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == true"), object: elements)
guard XCTWaiter.wait(for: [expectation], timeout: timeout) == .completed else {
XCTAssert(false, "Element does not exist")
}
}
}
1
2
3
4
5
6
7
8
class ConversationListRobot: Robot {

lazy private var newConversationButton = app.buttons["new_message"]

func newConversation() -> ConversationDetailRobot {
tap(newConversationButton)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class ConversationDetailRobot: Robot {

private var messageType = "Message"
lazy private var screenTitle = app.staticTexts["New \(messageType)"]
lazy private var contactField = app.textFields["contact"]
lazy private var cancel = app.buttons["Cancel"]
lazy private var messageField = app.textFields["messageField"]
lazy private var sendButton = app.buttons["send"]

func checkScreen(messageType: String) -> Self {
self.messageType = messageType
assertExists(screenTitle, contactField, cancel, messageField, sendButton)
return self
}

func enterContact(contact: String) -> Self {
tap(contactField)
contactField.typeText(contact)
return self
}

func enterMessage(message: String) -> Self {
tap(messageField)
messageField.typeText(message)
return self
}

func sendMessage() -> Self {
tap(sendButton)
return self
}

@discardableResult
func checkConversationContains(message: String) -> Self {
let messageBubble = app.staticTexts[message]
assertExists(messageBubble)
return self
}
}

These then allow us to chain together the functions to create tests, so our example above becomes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func test_sendNewiMessage() {

let message = "test message"

XCUIApplication().launch()

ConversationListRobot()
.newConversation()
.checkConversationContains(message: "Message")
.enterContact(contact: "iMessage Contact")
.checkScreen(messageType: "iMessage")
.enterMessage(message: message)
.sendMessage()
.checkConversationContains(message: message)
}

Immediately this is easier to read and understand for anyone at a glance, without having to know how the app functions, or how to write Swift.

Building Blocks

These high-level functions can then be chained together in different configurations, depending on what we want to test. Let’s say we want to try the same, but with an SMS contact. We’d make a new test, changing our contact to an ‘SMS Contact’ and on our second checkScreen() our message type would be ‘Message’.
Using this technique we can easily build several tests built from the building blocks we’ve created, we can try invalid contacts, we can check what happens if the user goes back to the message list, then creates a new conversation, we could try not entering a message at all, and many more. All using these basic functions with different values passed or in a different order, or maybe adding a new one where needed. And if the button used to send the message changes, we only need to change this in the implementation of sendMessage(), not in every test.

I’d highly reccomend getting to grips with XCUITesting, its an incredibly simple way to test your app presents to your users as you’re expecting, and so far as I can tell is criminally underused. The Robot Pattern has proved a clean, simple technique that has solved many issues for us, so its really worth considering if it will do the same for you. There’s very little documentation on using the Robot Pattern with Swift and iOS, but Faruk Toptaş provides some sample Android Kotlin code on his writeup.

Feel free to drop me an email if you have any questions about using this technique, rw@rwapp.co.uk