After reading this article, you can use
Quick and
Nimble in your projects!
Writing great, workable applications is one thing, but writing good tests that confirm the expected behavior of your application is much more difficult. In this article, we will look at one of the available approaches to testing applications, behavior-driven testing, using two extremely popular frameworks called
Quick and
Nimble .
You will learn about
behavior-driven testing : learn what it is, why it is an extremely powerful concept and how easy it is to write readable tests using the Quick and Nimble frameworks.
')
You will write tests for an amazingly simple and fun game called
AppTacToe , in which you play Tic Tac Toe against the computer, portraying an iOS character playing against an evil Android player!
Note: For a better understanding of this article, it is assumed that you have a basic knowledge of the Unit Testing topic and use XCTestCase.Although you can continue reading this article without this knowledge, it is still recommended that you read the article
iOS Unit Testing and UI Testing Tutorial to repeat the previously learned basics.
Start
The best way to start writing tests is to work on a real application, which in your case will be the AppTacToe game introduced earlier.
Download a start up project that contains everything you need to work; it already contains Quick and Nimble. After downloading, open
AppTacToe.xcworkspace .
Open
Main.storyboard and learn the basic structure of the application. It consists of two screens:
Board , in which the game itself takes place, and the “
Game Over screen ”
screen , which is responsible for displaying the result of the game.
Compile and run applications, and play one or two quick games to get familiar with the game.
You will also see useful logging, which is displayed in the console, duplicating the course of the game and displaying the final board, after the game is completed.
Note: Do not worry if you notice minor errors during the game; you fix them, while working on this article!Most of the business logic of the application is contained in one of two files:
Components / Board.swift : This file provides a logical implementation of the game Tic Tac Toe. There are no
UI interface elements associated with this game.
ViewControllers / BoardViewController.swift : This is the main game screen. He uses the above-mentioned
Board class for the game and is responsible for displaying the game state on the device screen and handling user interaction.
What you really need to test in this case is the business logic of the game, so you will write tests for the
Board class.
What is Behavior-Driven Testing?
The application consists of many code fragments. In traditional unit tests, you test all sorts of elements of each of these parts. You provide some data for a piece of code and claim that it returns the expected result.
The disadvantage of this approach is that it emphasizes the need to verify the internal operation of the application. This means that you spend more time testing implementation details, and then on the actual business logic, which is the real meat of your product!
It would be nice if you could just confirm that the application behaves as expected, regardless of how it was implemented.
Let's get acquainted with behavior-driven testing!
With behavior-driven testing (or
BDT ), your tests are based on user stories, which describe some specific expected actions of the application. Instead of checking out the details of the implementation, you are actually checking what is most important: is the application correctly performing user stories?
This approach makes tests extremely readable and supported, and also helps to describe the behavior of logical parts in the application to other developers who once have a chance to understand your code.
Here are some examples of user stories that you could write as part of the AppTacToe game:
Performing a single game action switches to another player.
Performing two game actions should switch back to the first player.
Performing a winning move should put the game into the Victory state.
Performing a turn after which there are no more moves puts the game into the Draw state.The role of Quick and Nimble in Behavior-Driven Testing
The tests are written based on the behavior management method and user stories, are simple sentences in English. This makes their understanding much easier compared to the usual Unit tests that you used to write.
Quick and Nimble provide an extremely powerful syntax that allows you to write tests that are read in the same way as regular sentences, allowing you to easily and quickly describe the behavior that you want to test. Inside, they work just like regular XCTestCase (s).
Quick provides most of the basic syntax and capabilities associated with writing behavior-driven tests, while Nimble is its companion framework. It provides additional expressive mapping and affirmation capabilities through Matchers, which you will learn about later in this article.
Anatomy of a quick test
Break one of the user stories into three articles based on
GWT — Given (the action / behavior you describe), When (the context of this action / behavior) and Then (what you expect to see):
Given / Given: User plays.
When / When: This is one move.
Then / Therefore: The turn must be transferred to another player.
In Quick, you use three functions: describe, context, and it.
Writing the first test
Quick test suites are called Specs, and each one you create must be inherited from QuickSpec, in the same way that you inherit from XCTestCase in tests. The test suite includes the main spec () method, which will contain all your test cases.
The start project already contains an empty test suite. Open the
AppTacToeTests / BoardSpec.swift file and look at the BoardSpec test suite inherited from QuickSpec and containing the only
spec () method in which you will work.
Note: When you open the
BoardSpec.swift file, you will see the error message No such module 'Quick' or 'Nimble'. Do not worry, as this is just an error in Xcode that is not related to the project. Your code will compile and run without any problems.
Begin by adding the following code inside the
spec () method:
var board: Board!
This code performs two actions:
- Defines the global variable board, which will be used in tests.
- Set a new Board instance for the board variable before each test, using the beforeEach closure with Quick's.
With a certain basic pattern, you can start writing the first test!
As conceived by this application, the game always starts with Cross (that is, either so or so., X), and the opponent will be Naught (that is, O).
Let's start with the first
user story mentioned above: After making the first move, the second player must make the next move.
Add the following code immediately after the closure of
beforeEach :
describe("playing") { // 1 context("a single move") { // 2 it("should switch to nought") { // 3 try! board.playRandom() // 4 expect(board.state).to(equal(.playing(.nought))) // 5 } } }
This is what this code does:
- describe () is used to determine which actions or behavior you will be testing.
- context () is used to define the specific context of the action that you will be testing.
- it () is used to determine the specific expected result for the test.
- You perform a random move using the playRandom () method in the Board class.
- You state that the Board status is changed to .playing (.nought) . At this stage, the equal () method from Nimble is used, which is one of many available functions that can be used to ascertain whether specific conditions match the expected value.
Note: You may have noticed a forced call to try and implicitly unwrapped an option to define test global variables. Although this option is usually not approved when writing code in the application itself, it is a fairly common practice when writing tests.Run tests by going to the
Product â–¸ Test menu panel or using the keyboard shortcut
Command + U.So, you will see your first test run.
Awesome!After completing the test, the Test navigator tab should look like this:
You have already noticed some interesting points by viewing the code. First of all, this is an extremely readable code. After reviewing the test code, anyone can read it relatively easily, as a simple sentence in English:
Performing a single move switches to the second player.
At this stage, you are introduced to the simple use of Nimble Matchers. Nimble uses these mappings so that you can get the expected test result in a very fast way, like simple sentences.
equal () is only one of the matching functions that is available in Nimble. You can even create your own functions.
Next test
The second user story - “After the second move should switch back to the first player” - sounds quite similar to the user story.
Add the following code immediately after the end of the previous
context () method, inside the
describe () curly brace:
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 previous one, but differs only in the fact that you make two moves instead of one.
Here is what the test performs:
- You define a new describe () to create a “two moves” context. You can have any number of describe () and context () blocks, and they may even be contained within each other. Since you are still testing the gameplay, you have added context inside the describe ("playing").
- You are making for successive moves.
- You state that the state of the board is now .playing (.cross) . Notice that this time you used the regular equality operator ==, instead of the .to (equal ()) syntax you used earlier. Matching Nimble's equal () provides its own overloaded operators that you can choose to your taste.
Arrange, Act & Assert
The tests you just wrote were relatively simple and straightforward. You make a single call on an empty board and state the expected result. Usually, most scenarios are more complex, which requires a little extra work.
The following two user stories will be more difficult:
The execution of the winning move should switch to the Victory state.
Performing an exit from the game does not produce any action, but only a transition to the state of Completion of the game.In both of these
user stories you need to take some steps on the game board so that you can check its status, your statement.
These tests are usually divided into three stages:
Arrange, Act and Assert .
Before planning tests, you should understand how the
Tic Tac Toe platform is implemented.
Board is modeled as an Array consisting of 9 cells addressed using indices from 0 to 8.
At each step, the player performs one move. To write a test to simulate a user's victory, you will need to bring the
Board to a state when the next move will be a winning one.
Now that you understand how the Board works, it's time to write this test.
Add the following code below your previous context “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 is what this code implements:
Arrange: You organize the Board to prepare it to the state when the next move will be a winning one. You do this by performing the moves of both players in turn; starting with X at point 0, at point 1, X at 3 and finally at 2.
Act: You are making a Cross (X) move to position 6. In the current state of the Board, this move should lead to a winning state.
Assert: You indicate that the game will be won by the Cross (X), and the Board will switch to the Win (.won (.cross)) state
Run the test again, use the keyboard shortcut
Command + U.Something is wrong; You have taken all the right steps, but the test unexpectedly failed.
Add the following code directly below the
expect () line to see the error:
print(board)
Displaying the
Board immediately after the
Assert block, you will receive a detailed explanation of this situation:
As you can see, the Board should be in the Victory state, but the test still does not work. Looks like you found a mistake.
Go to the
Project navigator tab and open
Board.swift . Go to the calculated
isGameWon property in line 120.
The code in this section checks all possible winning positions by rows, columns and diagonals. But looking at the columns, the code seems to have only 2 columns checked, and in fact one of the winning options is missing. Oops!
Add the following line of code directly below the
// Columns comment:
[0, 3, 6],
Run the tests again and enjoy the three green markers!
Such a scenario will be much harder to detect with the usual Unit tests. Since you are using behavior-based testing, you actually tested a specific use case for the application and found an error. The fix for the main implementation fixed the tested behavior, resolving the problem your user story was experiencing.
Note: While working on one particular test or a specific test context, you may not want to run all your tests at once, so that you can focus specifically on one test.
Fortunately, Quick provides a very easy way to do this. Just add f (stands for focus) before any of the names of the test functions — with it (), context, and describe (), become fit (), fcontext () and fdescribe ()
For example, after replacing it (“should switch to won state”) with fit (“should switch to won state”), only this particular test will be run, skipping the rest of the test suite. Just remember to delete it after you finish, otherwise only part of your tests will work!Little exercise
Time to call. You have one last user story that you haven’t yet tested: Running a turn after which there are no more moves puts the game into the Draw state.
Using the previous examples, write a test to verify the correctness of the Board definition.
Note: To reach the exit status, you can play the following positions in sequence: 0, 2, 1, 3, 4, 8, 6, 7 .In this state, playing position 5 should result in your board being in the draw state.
In addition, using the .draw method can confuse
Xcode . If so, use the full expression:
Board.State.draw .
If you didn’t manage to do this task, here’s the solution:
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 } }
Happy Way is not the only way
All the tests you have written so far have one thing in common: they describe the correct behavior of your application by following the
happy path . You have confirmed that when a player plays the correct moves, the game behaves correctly. But what about not very happy journey?
When writing tests, you should not forget about the concept of expected errors. You, as a developer, should be able to confirm the correct behavior of the Board, even if your player behaves incorrectly (for example, makes an unauthorized move).
Consider the last two user stories of this tutorial:
making a move that has already been made should cause an error.
making a move, after the game is won, should cause an error.
Nimble provides a convenient
resolver called
throwError () , which you can use to test this behavior.
Start by checking that a move that has already been played cannot be played again.
Add the following code right below the last
context () that you added, but still inside the
describe block
("playing") :
context() { it() { try! board.play(at: 0) // 1 // 2 expect { try board.play(at: 0) } .to(throwError(Board.PlayError.alreadyPlayed)) } }
This is what the code does:
- You perform a move to position 0.
- You play the move in the same position and expect it to roll the Board.PlayerError.alreadyPlayed . When you state that the error is displayed, expect accepts a closure in which you can run code that causes an error.
As you would expect from the Quick Tests, the statement reads the same as the English sentence: expected played (it is expected that the next game will cause an error “has already been played”).
Run the test suite again by going to
Product â–¸ Test or use the keyboard shortcut
Command + U.
The last user story you are going to study today will be: When making a move, after the game is won, it should cause an error.
This test should be relatively similar to the previous
Arrange, Act and Assert tests: you need to bring the board to a winning state, and then try to play one more step while the board is in this state.
Add the following code right below the last
context () that you added for the previous test:
context("a move while the game was already won") { it("should throw an error") { // Arrange try! board.play(at: 0) try! board.play(at: 1) try! board.play(at: 3) try! board.play(at: 2) try! board.play(at: 6) // Act & Assert expect { try board.play(at: 7) } .to(throwError(Board.PlayError.noGame)) } }
Based on the knowledge that you learned in this lesson, you should feel at home working with this test!
You bring the board to the Win state (.won (.cross)) by playing back 5 steps ... Then you
Act and Assert , trying to play the move while the board is already in the Win state, and waiting for the
Board.PlayError.noGame display.
Run your test suite again and pat your back after passing all these tests!

Custom mappings
When writing tests in this article, you already used several mappings embedded in Nimble: equal () (and its == overload operator), and
.throwError () .
Sometimes you want to create your own mappings to encapsulate some complex form of mapping or to increase the readability of some of your existing tests.
Think about how to improve the readability of the user story
"the winning move should switch the state to Win" mentioned earlier:
expect (board.state) == .won (.cross)Paraphrase this code as a sentence in English:
expect board to be won by cross (the board is expected to win the Cross (x)) . Then the test will have the following form:
expect (board) .to (beWon (by: .cross))Compilers in Nimble are nothing more than simple functions that return Predicate, where generic T is the type with which you compare. In your case, T will be of type Board.
In the project navigator, right-click the
AppTacToeTests folder and select New File. Select
Swift File and click
Next . Name your file
Board + Nimble.swift . Make sure you set the file correctly as a member of your
AppTacToeTests target task:

Replace the standard
import Foundation with the following three imports:
import Quick import Nimble @testable import AppTacToe
This code imports Quick and Nimble, and also imports your main target, so you can use the Board in your own comparison.
As mentioned earlier, the Matcher is a simple function that returns a
Predicate of the Board type.
Add the main part of the mapping below the import:
func beWon(by: Board.Mark) -> Predicate<Board> { return Predicate { expression in
This code defines the
beWon (by :) mapping that Predicate returns, so it matches the Board correctly.
Inside your function, you return a new instance of Predicate, passing it a closure with a single argument - expression - which is the value or expression you are comparing. The closure should return a PredicateResult.
At this point, you will see a compilation error, because the result has not yet been returned. Further it will be corrected.
To create a
PredicateResult , you must consider the following cases:
How
beWon (by :) mapping works

Add the following code inside the Predicate closure, replacing the comment, // Error !:
// 1 guard let board = try expression.evaluate() else { return PredicateResult(status: .fail, message: .fail("failed evaluating expression")) } // 2 guard board.state == .won(by) else { return PredicateResult(status: .fail, message: .expectedCustomValueTo("be Won by \(by)", "\(board.state)")) } // 3 return PredicateResult(status: .matches, message: .expectedTo("expectation fulfilled"))
Initially, this predicative implementation may seem confusing, but it's pretty simple if you follow it step by step:
- You are trying to evaluate an expression passed to expect () . In this case, the expression is the board itself. If the evaluation was failed, you return an unsuccessful PredicateResult with the appropriate message.
- You confirm that the state of the board is equal to .won (by) , where by is the argument passed to the Matcher function. If the status does not match, you return a PredicateResult error with the message .expectedCustomValueTo .
- Finally, if everything looks good and is checked, you return a successful PredicateResult .
This is it! Open
BoardSpec.swift and replace the following line:
expect(board.state) == .won(.cross)
using the new mapping:
expect(board).to(beWon(by: .cross))
Run the tests again by going to Product â–¸ Test or use the keyboard shortcut Command + U. You should see that all your tests still pass, but this time with the new Matcher!
What next?
Now you have the knowledge you need to write behavior-oriented tests in the application.
You learned all about testing
user stories , instead of testing implementation details, and how
Quick helps to achieve this. You also learned about
Nimble comparisons and even wrote your own comparison. Great!
To get started with
Quick and
Nimble in your own project, start with the installation guide and
select the installation method that is appropriate for your project.
When you have everything set up, and you want to learn more about Quick, click on the link titled
Quick's official documentation . Read also the
Readme Nimble for a wide range of comparisons and features.