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.

  1. 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.
  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, 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.
  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 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.

2 iphone messaging screenshots. one on left is empty white message inbox with messages written in black text. screenshot on right is new iMessage screen with blue text, blue circle button, and white and grey keyboard with black letters
    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.


Rob Whitaker, Software Engineer, UK TECH Software Engineering

iOS Engineer at Capital One. Watches wrestling, builds Lego, drinks beer, likes nerdy stuff. Self facilitating media node.

Related Content

abstract background in shades of purple, with hand holding a white iphone with a black screen and orange and grey tensorflow logo on it
Article | November 10, 2017
animated gif of blue magnifying glass examining a grey computer with white DevExchange screen. Grey rectangles arranged in lines are moving upwards as if someone were scrolling down the page
Software Engineering

Generic data sources in Swift

Article | July 5, 2017
Article | December 10, 2019