Engineering for Testability

Description: Unit testing is an essential tool to consistently verify your code works correctly. Discover techniques for designing the code of your app so that it can be easily tested. Find out the best practices for developing a test suite that evolves with your app and scales as your app grows.

  • Separate Logic and Effects
  • Pyramid of Tests (between UI and Unit tests there’s integration tests)

UI Tests Tips

Store parts of queries in variables.

Instead of this:

app.buttons["blue"].tap()
app.buttons["red"].tap()
app.buttons["yellow"].tap()
app.buttons["green"].tap()
app.buttons["purple"].tap()
app.buttons["orange"].tap()
app.buttons["pink"].tap() 

Write this:

func tapButton(_ color: String) {
  app.buttons[color].tap() 
}

let colors ["blue", "red", "yellow", 'green", "purple", "orange", "pink"]

for color in colors { 
  tapButton(color)
}

Wrap Complex queries in utility methods

Instead of this:

func testGameWithDifficultyBeginnerAndSoundOff() { 
  app.navigationBars["Game.GameView'].buttons["Settings"].tap()
  app.buttons["Difficulty"].tap()
  app.buttons["beginner"].tap()
  app.navigationBars.buttons["Back"].tap()
  app.buttons["Sound"].tap()
  app.buttons["off"].tap()
  app.navigationBars.buttons["Back"].tap()
  app.navigationBars.buttons["Back"].tap() 

  // test code 
}

Write this:

func testGameWithDifficultyBeginnerAndSoundOff() {
  app.navigationBars["Game.GameView"].buttons["Settings"].tap() setDifficulty(.beginner)
  setSound(.off) 
  app.navigationBars.buttons["Back"].tap() 

  // test code 
}

Then create a GameApp class:

class GameApp: XCUIApplication { 

  enum Difficulty { /* cases */ }
  enum Sound { /* cases */ }
  
  func setDifficulty(_ difficulty: Difficulty) { /* code */ }
  func setSound(_ sound: Sound) { /* code */ } 

  func configureSettings(difficulty: Difficulty, sound: Sound) {
    app.navigationBars["Game.GameView"].buttons["Settings"].tap() 
    setDifficulty(difficulty)
    setSound(sound)
    app.navigationBars.buttons["Back"].tap() 
  }
}

And get this:

func testGameWithDifficultyBeginnerAndSoundOff() {
  GameApp().configureSettings (difficulty: .beginner, sound: .off)

  // test code 
}

Now it’s super easy to change the configuration, and if in the future we have more settings we just need to update the GameApp class

In the configure settings func we can also write this:

func configureSettings(difficulty: Difficulty, sound: Sound) { 
  XCTContext.runActivity(named: "Configure Settings: \(difficulty), \(sound)") { _ in 
    app.navigationBars["Game.GameView"].buttons["Settings"].tap()
    setDifficulty(difficulty)
    setSound(sound) 
    app.navigationBars.buttons["Back"].tap() 
  }
}

So when the test run we can have more insights of what’s going on:

Missing anything? Corrections? Contributions are welcome 😃

Related

Written by

Federico Zanetello

Federico Zanetello

Software engineer with a strong passion for well-written code, thought-out composable architectures, automation, tests, and more.