Some good practices for XCUITest

Daniel Souza
3 min readDec 2, 2017

Make it easier to add more stable tests to your suite

When translating a Test Case (TC) to an automated script, you should think very carefully about what exactly you’re testing, to avoid the need of fixing the test every time a single label changes.

Let’s say you have a TC to assert a search-result flow:

TC123 - Validate Search TermTest Steps: 
1. Open app
2. Type "Xcode" on search field
3. Tap "OK" button
Expected Result:
3. The app should go to Results View with "Result for Xcode" as title.

Fragile Solution

let app = XCUIApplication()
app.textFields[“Search”].typeText(“Xcode”)
app.buttons[“OK”].tap()
XCTAssert(app.staticTexts[“Result for Xcode term”].exists)

It will pass this time, but it can start failing very soon. If the button’s label or textfield’s placeholder changes, you need to change your test, even though the flow still works. To avoid that fragility, you should use accessibility identifiers instead.

Accessibility Identifiers

You can set it either on Inteface Builder or via code.

//on your ViewController
btnSend.accessibilityIdentifier = "btn_send"
//test querying the element:
app.buttons[“btn_send”].tap()

View Transition

Before the view is actually presented, it may take some time. Time consumed by transition animation, by a loading view fetching information from the web, or any other process. Therefore, make sure to always wait for the view’s existence before asserting anything.

let resultView = app.otherElements["view_result"]
let viewExists = resultView.waitForExistence(timeout: 10)
XCTAssert(viewExists)
XCTAssert(app.staticTexts[“Result for Xcode term”].exists)

A great practice is to identify each UIViewController’s view, so that to assert the view is presented, you don’t depend on a label’s text that can change very easily, but on an accessibility identifier.

Page Object Pattern + Method Chaining

Wrap information about each ViewController in a different object (Page). That way the test can use that object instead of querying elements itself.

class SearchPage {
func type(query: String)
func tapSend()
}
//tests using Pages:
searchPage.type(query: "Xcode")
searchPage.tapSend()

To make tests more readable, you can use method chaining, like so:

class SearchPage {
func type(query: String) -> Self
func tapSend() -> ResultPage
}
//tests using Pages + Method Chaining
searchPage
.type(query: "Xcode")
.tapSend()

Framework

To make adding more tests easier, create a framework and a guideline that help your team. Here’s a start: https://goo.gl/bPYSyx

Useful logs

Lastly, you should provide some info when the test fail, specially if want to run the suite with Continuous Integration. I created a log function and I use closures to allow tests to run some actions after each step.

func type(query: String, completion: (() -> Void)? = nil) -> Self {
inputSearch.typeText(query)
log("typed query: \(query)")
completion?()
return self
}

You could use the optional completion closure for taking a screenshot after the step, for example.

searchPage
.type(query: "Xcode") {
takeScreeenShot()
}
.tapSend()

Til next version

I’m writing this post bit by bit and I will update it whenever I feel like I should. Those are good practices for now, but it may change tomorrow. Any feedback will be really appreciated! Thanks

--

--