Update: February 9, 2024. I’m going to walk this one back a bit. I’ve seen arguments against it that are valid. In particular, these are arguments against Clean Code. Does what I suggest make sense in the use case I had at the time of writing? Yes. Does it always? No.

This is the part where 99% of you tune out…

Page Object Model (POM). The king of UI test automation patterns. I’ve considered various other patterns when building new frameworks, but keep coming back to POM. It’s understood, it has never let me down, and always seems to fit.

When I build a framework, I build it with the test layer knowing NOTHING about the underlying framework. Instead, my pages do all the work and I simply call them. One question I always receive when introducing someone to a framework I’ve written is “Why do we create a function for tapping an object when we know the object and the tap function is simple enough to remember?”

How simple it could be: myButton.tap()

How difficult I make it: tapMyButton()with a supporting page function that typically does the same as the “simple” solution. func tapMyButton() { myButton.tap() }

Why? A number of reasons.

  1. When writing a new test, rather than having to remember the object’s name you can start typing tap and your IDE will likely list all the available tap functions that exist for that page. Easy!
  2. Sometimes it isn’t that simple. An example I came across this week was tapping a switch in iOS. There is a delay between iOS evaluating the tap and the tap being done. Around 10% of the time my test would proceed before the new value was registered and it wouldn’t be saved. The solution was to check the value prior to tapping, tapping, and then waiting until the tap was registered before allowing execution to proceed. You don’t want to have to repeat or remember that logic every time you tap such an object.

func tapMySwitch() {
  let initialValue = mySwitchValue
  mySwitch.tap()
  mySwitch.waitForSwitchValue(!initialValue)
}

This also required the writing of a wait function using a standard timer pattern that quits once the condition is met. For those of you looking for a full solution to that, I’ll oblige you, but please understand this code before pasting it in your project.

func waitForSwitchValue(_ expectation: Bool, timeout: Int = 3) {
  let value = (self.value as! NSString).boolValue
  var timerTicks = 0
  Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (timer) in
    timerTicks += 1
    if ((value == expectation) || (timerTicks == timeout)) {
      timer.invalidate()
    }
  }
}

A similar example I deal with somewhat often is a page that animates in a way that doesn’t report itself as idle; you’ll often have a check before it’s fully gone away. In that case, I have a waitForDisappearance()function. In a perfect world, the underlying testing framework (XCUITest in this case) wouldn’t have these shortcomings, but they all do so it’s up to you to understand and work around them.

I’m sure you still have questions, and I probably know what they are:

  • “Why not reserve these abstracted functions only for those odd cases?” Because nobody wants to have to remember which are the odd cases. You will make mistakes, you will forget, and you’ll bake that flakiness in again.
  • “Why not put a Thread.sleep(1) in there?” You know better. If you ever think that is the answer, you’re wrong, and there is a better way you’re not thinking of.

Following this theme, don’t do any data manipulation in your test class. Keep it somewhere else so you’ll know to reuse it, you won’t have little bits and pieces of logic all over the place. Keep as much “real” code out of your tests as possible. Ideally, someone new to your framework should be able to write a new test case from little to no understanding in less than an hour. Let your IDE work for you and others, and with abstraction, it does.