Robot Pattern testing for XCUITest
Using the Robot Pattern on iOS.
I recently spoke at iOSDevUK giving an overview of how we test our flagship iOS app at Capital One. The most common follow up question I’ve been asked is about using the Robot Pattern. I touched on the Robot Pattern briefly in the slides but clearly this is something others are interested in knowing more about
The Robot Pattern was designed by Jake Wharton at Square for testing in Kotlin. As a result, much of the information available focusses on Kotlin and Espresso testing. There’s relatively little about using the Robot Pattern for iOS online. However, the Capital One UK Mobile team settled on using the Robot Pattern as it meant a consistent approach between our Android and iOS testing. Here’s how it works for an iOS app.
Why use the Robot Pattern
There are three big reasons to use the Robot Pattern when writing XCUI Tests.
- Ease of understanding
We came to XCUITests from Calabash, where our tests were written in Cucumber. Cucumber is close to natural language. This means can quickly and easily read and understand what’s being tested without knowing exactly how the test works. Write the tests in native code and there’s already a learning curve to knowing what’s being tested and what isn’t. - 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, that’s 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. - 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 code that is easier to maintain, test, and improve. 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
Let’s imagine we’re writing UI tests for Apple’s built-in Messages app. If you load the app from a suspended state 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.
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 — there’s 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’d 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’.
- 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 that’s where we’ll spend most of our time.
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”)
}
}
}
class ConversationListRobot: Robot {
lazy private var newConversationButton = app.buttons[“new_message”]
func newConversation() -> ConversationDetailRobot {
tap(newConversationButton)
}
}
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, message Field, 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:
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 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:
- Invalid contacts.
- Returning to the message list.
- Attempting to send a blank message.
- Sending media
All using these basic functions with different values passed, or values in a different order, or maybe adding new values 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.
Conclusion
I’d highly recommend getting familiar with XCUITesting, it’s an incredibly simple way to test whether what your app presents to your users is what you’re expecting. And so far as I can tell, it’s criminally underused. The Robot Pattern has proved a clean, simple technique that has solved many issues for our team. It’s really worth considering whether it will do the same for you.
While there’s very little documentation on using the Robot Pattern with Swift and iOS, Faruk Toptaş provides some sample Android Kotlin code on his writeup, and the Robot Pattern’s designer Jake Wharton has a great introductory talk on the decisions behind his design.