Behavior-Driven Testing Tutorial for iOS with Quick & Nimble

In this behavior-driven testing tutorial, you’ll learn how to write tests for iOS apps and games using Quick and Nimble. By Shai Mishali.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Your Second Test

The second user story — “playing two moves should switch back to cross” — sounds fairly close to the first.

Add the following code right after the end of your previous context(), inside the closing curly brace of describe():

context("two moves") { // 1
  it("should switch back to cross") {
    try! board.playRandom() // 2
    try! board.playRandom()
    expect(board.state) == .playing(.cross) // 3
  }
}

This test is similar to the last one, different in the fact you’re playing two moves instead of one.

Here’s what it does:

  1. You define a new context() to establish the “two moves” context. You can have as many context()s and describe()s as you want, and they can even be contained within each other. Since you’re still testing gameplay, you added a context inside describe("playing").
  2. You play two consecutive random moves.
  3. You assert the board’s state is now .playing(.cross). Notice that this time you used the regular equality operator ==, instead of the .to(equal()) syntax you used earlier. Nimble’s equal() matcher provides its own operator overloads that let you choose your own flavor/preference.

Arrange, Act & Assert

The tests you’ve just written have been relatively simple and straightforward. You perform a single call on an empty Board, and assert the expected result. Usually, though, most scenarios are more complex, thus requiring a bit of extra work.

The next two user stories are more complex:

  • Playing a winning move should switch to the won state.
  • Playing a move leaving no remaining moves should switch to the draw state.

In both of these user stories, you need to play some moves on the board to get it into a state where you can test your assertion.

These kind of tests are usually divided into three steps: Arrange, Act and Assert.

Before you plan your tests, you must understand how the Tic Tac Toe board is implemented under the hood.

The board is modeled as an Array consisting of 9 cells, addressed using indices 0 through 8.

On each turn, a player plays a single move. To write a test for the winning user story, you’ll need to play both moves to bring the board to a state where the next move would be a winning move.

Now that you understand how the board works, it’s time to write the winning test.

Add the following code below your previous “two moves” context():

context("a winning move") {
  it("should switch to won state") {
    // Arrange
    try! board.play(at: 0)
    try! board.play(at: 1)
    try! board.play(at: 3)
    try! board.play(at: 2)

    // Act
    try! board.play(at: 6)

    // Assert
    expect(board.state) == .won(.cross)
  }
}

Here’s what this does:

  • Arrange: You arrange the board to bring it to a state where the next move would be a winning move. You do this by playing the moves of both players at their turn; starting with X at 0, O at 1, X at 3 and finally O at 2.
  • Act: You play Cross (X) at position 6. In the board’s current state, playing at position 6 should cause a winning state.
  • Assert: You assert the board’s state to be equal to won by cross (e.g. .won(.cross))

Run the test suite again by going to Product ▸ Test or using the Command + U shortcut.

Something is wrong; you played all of the right moves, but the test failed unexpectedly.

Add the following code immediately below the expect() code line to see what went wrong:

print(board)

By printing the board immediately after the assertion you will get better clarity of the situation:

As you can see, the board should be in a winning state, but the test is still failing. Seems like you found a bug.

Switch to the Project navigator and open Board.swift. Go to the isGameWon computed property on line 120.

The code in this section tests for all possible winning positions across rows, columns and diagonals. But looking at the columns section, the code seems to only have 2 columns tested, and is actually missing one of the winning options. Whoops!

Add the following line of code immediately below the // Columns comment:

[0, 3, 6],

Run your test suite again and bask in the glory of three green checkmarks!

This kind of scenario would be much harder to detect with regular unit tests. Since you’re using behavior-driven testing, you actually tested a specific use case of the app and detected a fault. Fixing the underlying implementation fixed the tested behavior, resolving the issue your user story was experiencing.

Note: While working on one specific test or a specific context of tests, you might not want to run all of your tests at once to enable you to focus specifically on working on one test.

Fortunately, Quick provides an extremely easy way to do this. Simply add f (stands for focused) before any of the test function names – having it(), context() and describe() become fit(), fcontext() and fdescribe().

For example, changing it("should switch to won state") to fit("should switch to won state"), will only run that specific test, skipping the rest of your test suite. Just don’t forget to remove it after you’re done, or only part of your tests will run!

Note: While working on one specific test or a specific context of tests, you might not want to run all of your tests at once to enable you to focus specifically on working on one test.

Fortunately, Quick provides an extremely easy way to do this. Simply add f (stands for focused) before any of the test function names – having it(), context() and describe() become fit(), fcontext() and fdescribe().

For example, changing it("should switch to won state") to fit("should switch to won state"), will only run that specific test, skipping the rest of your test suite. Just don’t forget to remove it after you’re done, or only part of your tests will run!

A Short Exercise

Time for a challenge. You have one last user story you haven’t tested yet: “Playing a move leaving no remaining moves should switch to draw state”

Using the previous examples, write a test to verify the board correctly detects a Draw state.

Note: To get to a Draw state you can play the following positions sequentially: 0, 2, 1, 3, 4, 8, 6, 7.
At this state, playing position 5 should result in your board being in a draw state.

Also, matching the state with .draw might confuse Xcode. If that is the case, use the full expression: Board.State.draw.

Note: To get to a Draw state you can play the following positions sequentially: 0, 2, 1, 3, 4, 8, 6, 7.
At this state, playing position 5 should result in your board being in a draw state.

Also, matching the state with .draw might confuse Xcode. If that is the case, use the full expression: Board.State.draw.

Tap the button below to see the full solution.

[spoiler title=”Test for Draw State”]

context("a move leaving no remaining moves") {
  it("should switch to draw state") {
    // Arrange
    try! board.play(at: 0)
    try! board.play(at: 2)
    try! board.play(at: 1)
    try! board.play(at: 3)
    try! board.play(at: 4)
    try! board.play(at: 8)
    try! board.play(at: 6)
    try! board.play(at: 7)

    // Act
    try! board.play(at: 5)

    // Assert
    expect(board.state) == Board.State.draw
  }
}

[/spoiler]