Write Clean and Maintainable XCUITests with Screen Chaining

Fran Roig
3 min readJul 6, 2024

--

UI testing can be particularly challenging when your app features numerous screens, trays, dialogs and intricate interactions. As the user interface grows in complexity, with many pages and elements, the associated automated tests also become more intricate. To address these challenges, developers have adopted a design pattern called the Page Object Model.

One effective method to enhance the maintainability and readability of UI tests is by implementing screen chaining. In this article, we’ll delve into how to incorporate screen chaining into your XCTest UI tests.

What is Screen Chaining?

Screen chaining is a design pattern where each screen in your app is represented by a class, and navigating from one screen to another returns an instance of the corresponding screen class. This approach enables you to write fluent and readable test code by chaining method calls to simulate user interactions seamlessly.

func testTool() {
BaseTest.mainScreen
.openFirstTool { firstToolScreen in
toolScreen
.useToolOptions { toolOptionsScreen in
toolOptionsScreen
.selectFromDropdown(option: .sampleDropdown, verify: true)
.tapToToggle(propertySwitch: .someOption)
.closeScreen()
}
.tap(button: .save)
.checkForNotification(ToolScreen.ErrorStrings.someError.rawValue, expected: false)
.closeScreen()
}
.openSecondTool { secondToolScreen in
secondToolScreen
.scrub(sliderPopover: .slider, to: 0.5, direction: .up)
.verifySomething()
.closeScreen()
}
}

By abstracting screen interactions into individual classes and using method chaining, it simplifies the process of writing tests, making them more intuitive and maintainable. This approach allows QE Devs to focus on building a robust automation framework and page models, leaving the actual test case writing to either developers or QEs. This division of responsibilities enhances collaboration and efficiency within the testing process.

Setting Up a BaseTest Class

To streamline the testing process, you can create a BaseTest class that inherits from XCTestCase. Each test case will inherit from this BaseTest class. Inside the BaseTest class, you initialize the MainScreen, and place common setup and teardown functions such as setUpWithError, tearDown, etc.

In this setup, BaseTest provides a centralized place for initializing the main screen and holding a reference to the current XCTestCase instance. This ensures that all test cases have a consistent starting point and access to common setup and teardown logic.

This is also where the XCTCRef is attached, ensuring that each screen class has access to the current running XCTestCase

class BaseTest: XCTestCase {

static let app = XCUIApplication()
static var mainScreen: MainScreen!
static var XCTCRef: BaseTest!

override func setUpWithError() throws {
continueAfterFailure = false
BaseTest.mainScreen = MainScreen()
BaseTest.XCTCRef = self
}

override func tearDownWithError() throws {
// Clean up code here
}

[Other required code here...]
}

Setting Up the MainScreen Class

Let’s define a MainScreen class that will serve as the main point of entry for your app. This class allows navigation to other screens or the use of different tools within your app.

class MainScreen {

@discardableResult
func openFirstTool() -> FirstToolScreen {
app.buttons["FirstTool"].tap()
return FirstToolScreen()
}

@discardableResult
func openSecondTool() -> SecondToolScreen {
app.buttons["SecondTool"].tap()
return SecondToolScreen()
}

[Other code here...]
}

Conclusion

By encapsulating screen interactions within screen classes and using method chaining, you can create fluent and intuitive test scripts. This pattern not only enhances test readability but also encourages better organization and reusability of your test code.

Setting up the BaseTest class will ensure that all tests start with a fresh instance of the main screen, and you can easily include any common setup or teardown logic in one place. Additionally, defining actions to be performed on each screen within their respective classes and having most methods return an object representing the resulting screen allows for seamless method chaining. This approach significantly improves the readability of test code, enabling developers and QEs to write tests that are easy to understand and maintain.

Happy Testing!

--

--