Sitemap

The Robot Testing Pattern

11 min readAug 28, 2024

--

The theory about the robot testing pattern and hands-on example of UI automation of the iOS app using the XCUITest framework.

To follow along you need to have Apple’s programming software — Xcode (available only on macOS). Here is a link to download the project.

To showcase the best practices, I created a small app on top of which I will create two automated tests. The application is really basic: two tabs (screens) with some elements. One tab contains a button, which is navigating to a different screen, just like a real app would do.

Architecture

You have probably heard about MVP, MVC and MVVM, which are application architectures. These are the go-to ways of building big, scalable apps.

But what about UI testing?

Quality has a crucial role while building software, so we should have something similar for this purpose. Establishing processes, ways of working, etc. is very important, especially in big projects. As it grows, the cost of maintaining rises.

The Robot Pattern — UI testing architecture — was designed in 2016 by Jake Wharton and first seen in Kotlin. This approach is all about making UI testing clearer and more structured, but the cool thing is, it’s not just limited to Kotlin. In fact, Robot Pattern can be used in various programming languages.

What is it all about?

The main idea behind this pattern is the separation of concerns — the what from the how.
The tests are all about ‘what’ needs to be done, while the robots handle ‘how’ it’s done. It keeps your tests separate from the main actions controlling the app’s views.
Let’s imagine that you have 3 tests testing screen A on your application:

Press enter or click to view image in full size
Testing implementation without Robot pattern

The problem with this implementation is that if something changes on screen A, all tests are broken. That’s why we need a middleman (robot) to interact with the screen. Inside, there are defined elements, actions, etc. for the tested screen.

Testing implementation with Robot pattern

If you are creating a new Xcode project and want to include a test and have template test code, you need to select them during project setup. Make sure you select one of the options (screen from Xcode 16.1 beta):

Creating a new project, selecting the Testing system

In the previous Xcode versions, you might have only one option to select regarding the test framework. (Swift Testing is a new testing framework introduced during WWDC 2024)

Robot UI testing for iOS using the XCUITest framework

To showcase the best practices I created a small app on top of which I will create two automated tests. The application is a basic one: two tabs (screens) with some elements on them. One tab contains a button that navigates to a different screen, just like a real app.

Presentation of the iOs app

Let’s use what we know — robots are representations of what we can see, so they are the classes for each screen. The first one that we can create is the basic one with the common navigation options — bottom tab buttons “Home” and “Cart”. Let’s name it “App”:

class App {
private var homeTab = app.buttons["Home"]
private var homeTabImage = app.images["house.fill"]

private var cartTab = app.buttons["Cart"]

[rest of the code]
}

To define the elements on the screen we can use the Accessibility Inspector app or the terminal to see the hierarchy of the app and elements on the screen.
The first option is the built-in macOS application. To use it we need to run both apps — the one that we want to scan and the “scanner”. To see the details of any element, click the “crosshair” button on the top right side of the Accessibility Inspector and then click the element we want to scan. In this example, I selected the Home Tab button:

Press enter or click to view image in full size
Checking the element details using the Accessibility Inspector

The second option is using the terminal and this is what I choose daily. To use it, we must run any test, set a breakpoint, preferably right at the beginning of the test, and be on the screen whose elements we want to have listed.
With the breakpoint, in the console, we can use the command:
po XCUIApplication():

Press enter or click to view image in full size
The app structure using the command line

Now, we have all the objects on the screen listed out and we can use the list to create the robots.

To define any element such as a button, staticText, image, etc. we need to know the identifier or label of a certain element. In a basic app, there are no specially created identifiers, but in a real application, there are certain scenarios in which an identifier is the best option to select and we should create them ourselves or ask developers to create them.
We want to define navigation buttons that can be used in the tests, that’s why they are in the “default” robot. This is going to be the beginning of every test. To create a button, make a private variable which is going to be the element we want, in this case, a button.
To make sure that we define the correct element, we need to write:

app.buttons["Home"]
  • app — this is the variable that defines our app:
let app = XCUIApplication()
  • buttons[identifier or label] — here we need to specify which element we want to have; here we want the buttons. If we wanted to have e.g. an image or staticText, we need to use the appropriate element. In square brackets provide an identifier or label for the element, according to the app hierarchy.

I also defined the image for the Home Tab, but why not for the Cart Tab? This is because the default screen of this training app is the Home one, and in the current implementation we can check the image only for the selected tab. Of course, we could define it in the robot (class) for
the Home Tab and check it when we operate on this screen, but because this is the default screen, I’m checking it right away on the app launch. Next is the initialization of this robot (class) and functions for checking the elements.

init() {
checkElements()
}

private func checkTappableElements() {
[
cartTab,
homeTab
].forEach { XCTAssertTrue($0.isHittable && $0.isEnabled) }
}

private func checkDefaultTabSelected() {
XCTAssertTrue(homeTab.isSelected)
XCTAssertTrue(homeTabImage.exists)
}

private func checkElements() {
checkTappableElements()
checkTabSelected()
}

The initialization is the part of the code that is going to be run whenever we “enter” this screen.

This is the perfect opportunity to check if all of the default elements are displayed etc. checkElements() function is the main container for another, more detailed one. In the init() of the App robot, we check:

  • checkTappableElements() does exactly what the name suggests — checks all of the tappable elements which in this case are all the buttons — home and cart. I defined an array with the elements of the same type (buttons), and by using forEach and XCTAsserTrue I’m checking one by one if elements are hittable and enabled,
  • checkDefaultTabSelected() does exactly what the name suggests — checks if the default tab is selected and if the correct image is there. Using XCTAsserTrue I’m checking if the homeTab button is selected and if the homeTabImage is there.

The approach of checking the default screen elements in the init() is very common and convenient. But because my test app is very small, on different robots I will make the same functions but I will use them “manually” instead of checking certain objects in init() for the sake of demonstration and to showcase the best practices to write the tests descriptively.

With that said, let’s implement the robot for the Home screen. The same as in the App screen, let’s define all of the objects on the screen:

class HomeScreen {
private lazy var navigationBar = app.navigationBars.firstMatch
private lazy var screenTitle = navigationBar.staticTexts["Home"]
private lazy var navbarButton = navigationBar.buttons["Navbar button"]
private lazy var screenText = app.staticTexts["This is the text for home
tab"]
private lazy var screenButton = app.buttons["Go to another screen"]
private lazy var cartTab = app.buttons["Cart"]
}

The difference is that I defined the first matching object: navigationBar, which I created according to the app hierarchy. Thanks to that I can create a button on the top navigation bar. Other elements are created in the same way as on the App robot.

Get Kacper Rubaszewski’s stories in your inbox

Join Medium for free to get updates from this writer.

Note that here we have another element: staticText.

Other sections of this robot are the functions to check certain elements. As I already mentioned, I’m not going to use the init(), to “manually” check the elements in the test (which I’m going to describe later). The naming convention of the first functions is pretty much the same as in the first class. The difference is that those functions return Self:

@discardableResult
func checkTappableElements() -> Self {
[
screenButton,
navbarButton
].forEach { XCTAssertTrue($0.isHittable && $0.isEnabled) }
return self
}

@discardableResult
func checkTextElements() -> Self {
[
screenTitle,
screenText
].forEach { XCTAssertTrue($0.exists) }
return self
}

This is because when we are explicitly using the functions to use them in the test, we need to return to the same place when we are.

We need to think about this situation like:

“Right now I’m checking the buttons (checkTappableElements()), so when this step is finished we are still on the same screen. We need to “say” that, that’s why we return self — this exact screen. Then we can use another function defined on the class which checks the text elements (checkTextElements()).”

The @discardableResult is intended to avoid warning that the result of using a function that returns something is not used.

The last functions are defined to be able to go to the different screens (robots):

@discardableResult
func goToAnotherSceen() -> AnotherScreen {
screenButton.tap()
return AnotherScreen()
}

@discardableResult
func openCartTab() -> CartScreen {
cartTab.tap()
return CartScreen()
}
  • goToAnotherSceen() — a function to click the button on the Home Screen and open Another Screen,
  • openCartTab() — a function that enables going from the Home Tab to the Cart Tab.

Notice what these functions do — click the button and return the appropriate class, which we know is the robot. Similar functions are defined also in the App robot with appropriate actions:

class App {

[code presented above]

@discardableResult
func openCartTab() -> CartScreen {
cartTab.tap()
return CartScreen()
}

@discardableResult
func openHomeTab() -> HomeScreen {
homeTab.tap()
return HomeScreen()
}

We are nearly there. Now we need to create the test class in which we are going to use the robots. These classes are usually named after the area of the app which they cover. In my case, I have the default file and class name which has been created with the Xcode project and named after the project name. I’ve deleted all the border plate codes.

Let’s describe the test case:

import XCTest

final class RobotPatternUITests: XCTestCase {

override class func setUp() {
app.launch()
}

func test_go_to_home_screen_go_to_another_screen_and_come_back() {
App()
.openHomeTab()
.checkTappableElements()
.checkTextElements()
.goToAnotherSceen()
.goBackToHomeSceen()
}
}

The main class needs to conform to the XCTestCase interface (protocol). Then we need to override the setup() function and launch the app, on top of which we want to run the test.
Without that, the test is going to run but it’s going to fail because it’s not able to find the elements (you can’t find something if it’s not being run).

All the functions which start with “test” are test scenarios. Thanks to the “test” at the beginning of the name, Xcode knows that they can be run. The rest of the name should be descriptive to know what the test is doing by reading the name of it. Snake case should be used (lowercase, spaces are replaced with underscores). The beginning of the test is to specify the main robot class, which in this case is App(). As described above, when the app is launched and the test starts executing, App robot init() is triggered to check whatever is there as the class is initialized. Then we can do whatever is
defined on that App robot, so in this scenario open the Home Tab.
To do that, we need to use the function, so that is why I’m using:
.openHomeTab().

We remember that the basic screen is the Home Tab, so you could ask:

“Why do we need to “manually” open it, can’t we just start clicking the elements there?“

That’s a good question. We need to remember that every robot is the representation of the app’s screens, so whenever we open the App, we are just in this robot (App robot) with limited elements that we defined.
To be able to perform an action on the Home Tab, we need to “go” to the Home Screen robot (Home Screen class) to have all of the objects. Then we
can perform other actions defined in the Home Screen robot, which is to check the text elements and the buttons:

  • .checkTappableElements(),
  • .checkTextElements().

Now we can move to Another Screen and to do so, I’m using the function which is tapping the button on the Home Screen:

Button to navigate to another screen

This “moves” us to Another Screen robot which enables us to check elements on that screen.
The last step is to go back to the Home Screen by tapping the back button:

Navigation bar — title and “back” button

This is triggered by the goBackToHomeSceen() function. That’s the end of the first test.
The last test is the flow to open the Cart Screen, check all the elements, and open the Home Screen again. The test looks like this:

[first test presented above]

func test_go_to_cart_screen_chech_open_home_screen() {
App()
.openCartTab()
.checkTabSelected()
.checkTappableElements()
.checkTextElements()
.openHomeTab()
}

The beginning of the test is the same as in the first example. Then we “go” to the Cart Screen using the .openCartTab() function. The next function is to check if the tap button is selected and if the image exists — .checkTabSelected().
The next two functions do the same thing as in the first test — check the text elements and the buttons (but on the Cart robot):

  • .checkTappableElements(),
  • .checkTextElements().

The last step is to open the Home Tab, to do so we need to use the proper function — .openHomeTab(). That’s the end of the second test.

In this article, we’ve delved into what the Robot Testing Pattern is, understanding the key concepts and benefits. Then presented utilizing this framework for iOS applications.

Automated testing is not just a tool. It’s a strategic asset that can transform your development workflow, ensuring your app stands out in a competitive market. By integrating robust testing methodologies, you ensure your app not only meets functional requirements but also delivers a seamless user experience.

If you haven’t done so yet, you can download the whole project with the source code here. I encourage you to check it out and practice by implementing your own test cases.

Also, you can let me know if you want another article to delve more into the iOs automation with custom functions to scroll, find elements using labels or identifiers (prettier, without using basic prefixes like app etc.), and more.
On the other hand, I can create the same tests using the Appium framework. Let me know! Hit me up with the ideas!

In case of any questions, you can contact me via LinkedIn or email.

Thank you so much for reading my first article.

Best regards, Kacper.

--

--

Kacper Rubaszewski
Kacper Rubaszewski

Passionate engineer with a willingness to learn new things and talk about them. Currently working as an iOS Automation Engineer (Team Lead) @ amp.