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

Writing beautiful, performant applications is one thing, but writing good tests that verify your application’s expected behavior is a much harder task. In this tutorial, you’ll examine one of the available approaches for testing applications, called behavior-driven testing, using two extremely popular frameworks named Quick and Nimble.

You’ll learn about behavior-driven testing: what it is, why it’s an extremely powerful concept, and how easy it is to write maintainable and readable tests using Quick and Nimble.

You’ll be writing tests for an amazingly simple and fun game called AppTacToe in which you play a game of Tic Tac Toe vs. the computer, portraying the iOS character playing against the evil Android player!

Note: This tutorial assumes basic knowledge of Unit Testing and using XCTestCase.

Even though you should be able to follow this tutorial without this prior knowledge, we do recommend checking out our iOS Unit Testing and UI Testing Tutorial if you want a refresher of the basics.

Getting Started

The best way to get started testing is to work on a real app, which in your case will be the beautiful AppTacToe game introduced earlier.

Use the Download Materials button at the top or bottom of this tutorial to download the starter project, which already has Quick and Nimble bundled, and open AppTacToe.xcworkspace.

Open Main.storyboard and examine the basic structure of the app. It contains two screens: the Board itself, where the game is played, and the Game Over screen, responsible for displaying the game’s result.

Build and run the app, and play a quick game or two to get the hang of it.

You’ll also see some useful logs printed to your console, portraying the game play and printing out the resulting board as the game ends.

Note: Don’t worry if you notice a minor bug while playing the game; you’ll fix this as you work your way through this tutorial!

Most of the app’s logic is contained in one of two files:

  • Components/Board.swift: This file provides the logical implementation of a Tic Tac Toe game. It has no UI associated with it.
  • ViewControllers/BoardViewController.swift: This is the main game screen. It uses the aforementioned Board class to play the game itself, and is solely responsible for drawing the state of the game on screen and handling user interaction with the game.

What you really want to test in this case is the the logic of the game, so you’ll be writing tests for the Board class.

What is Behavior-Driven Testing?

An application is comprised of many pieces of code. In traditional unit tests, you test the ins-and-outs of every one of these pieces. You provide some inputs to some piece of code, and assert that it returns the result you expect.

A downside of this approach is that it emphasizes testing the inner workings of your applications. This means you spend more time testing implementation details then actual business logic, which is the real meat of your product!

Wouldn’t it be nice if you could simply verify your application behaves as expected, regardless of how it was implemented?

Enter behavior-driven testing!

Behavior Driven Tests vs. Unit Tests

In behavior-driven testing (or BDT), your tests are based on user stories that describe some specific expected behavior of your application. Instead of testing implementation details, you’re actually testing what matters: does your app deliver your user stories correctly?

This approach makes tests extremely readable and maintainable, and helps describe the behavior of logical portions in your application to other developers who might be lucky enough to go through your beautiful code one day.

Some examples of user stories you might end up writing as part of the AppTacToe game might be:

  • Playing a single move should switch to other player.
  • Playing two moves should switch back to the first player.
  • Playing a winning move should switch to a Won state.
  • Playing a move leaving no remaining moves should switch to a Draw (Tie) state.

Quick and Nimble’s role in Behavior-Driven Testing

Tests written in a behavior-driven way are based on user stories, which are regular sentences, written in plain English. This makes them much easier to understand when compared to the usual unit tests you’re accustomed to writing.

Quick and Nimble provide an extremely powerful syntax that let you write tests that read exactly like regular sentences, allowing you to easily and swiftly describe the behavior you wish to verify. Beneath the surface, they work exactly like regular XCTestCase(s).

Quick itself provides most of the basic syntax and abilities related to writing tests in a behavior-driven way, while Nimble is its companion framework. It provides additional expressive matching and assertion abilities via Matchers, which you’ll learn about a bit later in this tutorial.

The Anatomy of a Quick Test

Break up one of the user stories into three clauses based on GWTGiven (the action/behavior you’re describing), When (the context of that action/behavior) and Then (what you expect to happen):

  • Given: User is playing.
  • When: It is a single move.
  • Then: The game should switch to other player.

In Quick, you use three functions as the counterparts of each: describe, context and it.

Anatomy of a Quick test


Your First Test

In Quick, test suites are named Specs, and every test suite you create starts off with a class inheriting from QuickSpec in the same way you inherit from XCTestCase in non-Quick tests. The test suite includes a main method, spec(), that contains the entirety of your test cases.

The starter project already contains an empty test suite. Open AppTacToeTests/BoardSpec.swift and take a look at the BoardSpec test spec, inheriting from QuickSpec and containing a single method, spec(), in which you’ll be writing test cases and expectations.

Note: When you open the BoardSpec.swift file, you might see an error saying No such module 'Quick'. Worry not, as this is just a Xcode bug/glitch unrelated to your project. Your test code will compile and work with no issues whatsoever.

Start by adding the following code inside spec():

var board: Board! // 1

beforeEach { // 2
  board = Board()

This code performs two actions:

  1. Defines a global board to be used across test cases.
  2. Resets that board to a new instance of Board before every test using Quick’s beforeEach closure.

With some basic boilerplate out of the way, you can start writing your very first test!

For the purposes of this app, the game will always start with Cross (e.g. X), and the opponent will be Nought (e.g. O).

Let’s start with the first user story mentioned above: playing a single move should switch to nought.

Add the following code immediately after the end of the beforeEach closure:

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

Here’s what this does, line by line:

  1. describe() is used to define what action or behavior you’ll be testing.
  2. context() is used to define the specific context of the action you’ll be testing.
  3. it() is used to define the specific expected result for the test.
  4. You play a random move on the Board class using playRandom().
  5. You assert the board’s state has been changed to .playing(.nought). This step uses the equal() matcher from Nimble, which is one of many available functions you can use to assert a matching of specific conditions on an expected value.
Note: You might have noticed the forced try call and implicitly unwrapped optional to define test globals. While this is usually frowned upon when writing regular code for apps, it is a relatively common practice when writing tests.

Run your tests by either navigating in the menu bar to Product ▸ Test or by using the Command-U shortcut.

You’ll see your very first test pass. Awesome!

Your Test navigator will look like this:

You can already notice a few interesting points by going through this code. First of all, it is extremely readable. Going through the lines of code, any person could relatively easily read it as a plain English sentence:

“Playing a single move should switch to nought. Play a random move and expect the board’s state to equal nought playing.”

You were also just introduced to your first usage of Nimble Matchers. Nimble uses these Matchers to let you express the expected outcome of your test in a very fluent, sentence-like way. equal() is just one of the matcher functions available in Nimble and, as you’ll see shortly, you can even create your own custom ones.