Unit Testing on macOS: Part 1/2

In this Unit testing tutorial for macOS you’ll learn how unit tests can help you to write stable code and give you the confidence that changes don’t break your code. By Sarah Reichelt.

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

Choosing Which Tests to Run

So which method should you use for running your tests? Single, class or all?

If you are working on a test, it is often useful to test it on its own or in its class. Once you have a passing test, it is vital to check that it hasn’t broken anything else, so you should do a complete test run after that.

To make things easier as you progress, open DiceTests.swift in the primary editor and Dice.swift in the assistant editor. This is a very convenient way to work as you cycle through the TDD sequence.

That completes the second step of the TDD sequence; since there is no refactoring to be done, it’s time to go back to step 1 and write another failing test.

Testing for nil

Every Dice object should have a value which should be nil when the Dice object is instantiated.

Add the following test to DiceTests.swift:

  // 1
  func testValueForNewDiceIsNil() {
    let testDie = Dice()

    // 2
    XCTAssertNil(testDie.value, "Die value should be nil after init")
  }

Here’s what this test does:

  1. The function name starts with 'test', and the remainder of the function name expresses what the test checks.
  2. The test uses one of the many XCTAssert functions to confirm that the value is nil. The second parameter of XCTAssertNil() is an optional string that provides the error message if the test fails. I generally prefer to use descriptive function names and leave this parameter blank in the interests of keeping the actual test code clean and easy to read.

This test code produces a compile error: "Value of type 'Dice' has no member 'value'".

To fix this error, add the following property definition to the Dice struct within Dice.swift:

  var value: Int?

In DiceTests.swift, the compile error will not disappear until the app is built. Press Command-U to build the app and run the tests which should pass. Again there is nothing to re-factor.

Each Dice object has to be able to “roll” itself and generate its value. Add this next test to DiceTests.swift:

  func testRollDie() {
    var testDie = Dice()
    testDie.rollDie()

    XCTAssertNotNil(testDie.value)
  }

This test uses XCTAssertNotNil() instead of XCTAssertNil() from the previous test.

As the Dice struct has no rollDie() method, this will inevitably cause another compile error. To fix it, switch back to the Assistant Editor and add the following to Dice.swift:

  func rollDie() {

  }

Run the tests; you’ll see a warning about using var instead of let along with a note that XCTAssert has failed this time. That makes sense, since rollDie() isn’t doing anything yet. Change rollDie() as shown below:

  mutating func rollDie() {
    value = 0
  }

Now you are seeing how TDD can produce some odd code. You know that eventually the Dice struct has to produce random dice values, but you haven’t written a test asking for that yet, so this function is the minimum code need to pass the test. Run all the tests again to prove this.

Developing to Tests

Put your thinking cap on — these next tests are designed to shape the way your code comes together. This can feel backwards at first, but it’s a very powerful way to make you focus on the true intent of your code.

You know that a standard die has six sides, so the value of any die after rolling should be between 1 and 6 inclusive. Go back to DiceTests.swift and add this test, which introduces two more XCTAssert functions:

  func testDiceRoll_ShouldBeFromOneToSix() {
    var testDie = Dice()
    testDie.rollDie()

    XCTAssertTrue(testDie.value! >= 1)
    XCTAssertTrue(testDie.value! <= 6)
    XCTAssertFalse(testDie.value == 0)
  }

one-sided_dice2

Run the tests; two of the assertions will fail. Change rollDie() in Dice.swift so that it sets value to 1 and try again. This time all the tests pass, but this dice roller won’t be of much use! :]

Instead of testing a single value, what about making the test roll the die multiple times and count how many of each number it gets? There won’t be a perfectly even distribution of all numbers, but a large enough sample should be close enough for your tests.

Time for another test in DiceTests.swift:

  func testRollsAreSpreadRoughlyEvenly() {
    var testDie = Dice()
    var rolls: [Int: Double] = [:]

    // 1
    let rollCounter = 600.0

    for _ in 0 ..< Int(rollCounter) {
      testDie.rollDie()
      guard let newRoll = testDie.value else {
        // 2
        XCTFail()
        return
      }

      // 3
      if let existingCount = rolls[newRoll] {
        rolls[newRoll] = existingCount + 1
      } else {
        rolls[newRoll] = 1
      }
    }

    // 4
    XCTAssertEqual(rolls.keys.count, 6)

    // 5
    for (key, roll) in rolls {
      XCTAssertEqualWithAccuracy(roll,
                                 rollCounter / 6,
                                 accuracy: rollCounter / 6 * 0.3,
                                 "Dice gave \(roll) x \(key)")
    }
  }

Here’s what’s going on in this test:

  1. rollCounter specifies how many times the dice will be rolled. 100 for each expected number seems like a reasonable sample size.
  2. If the die has no value at any time during the loop, the test will fail and exit immediately. XCTFail() is like an assertion that can never pass, which works very well with guard statements.
  3. After each roll, you add the result to a dictionary.
  4. This assertion confirms that there are 6 keys in the dictionary, one for each of the expected numbers.
  5. The test uses a new assertion: XCTAssertEqualWithAccuracy() which allows inexact comparisons. Since XCTAssertEqualWithAccuracy() is called numerous times, the optional message is used to show which part of the loop failed.

Run the test; as you would expect, it fails as every roll is 1. To see the errors in more detail, go to the Issue Navigator where you can read what the test results were, and what was expected.

IssueNavigator

It is finally time to add the random number generator to rollDie(). In Dice.swift, change the function as shown below:

  mutating func rollDie() {
    value = Int(arc4random_uniform(UInt32(6))) + 1
  }

This uses arc4random_uniform() to produce what should be a number between 1 and 6. It looks simple, but you still have to test! Press Command-U again; all the tests pass. You can now be sure that the Dice struct is producing numbers in roughly the expected ratios. If anyone says your app is cheating, you can show them the test results to prove it isn’t!

Job well done! The Dice struct is complete, time for a cup of tea...

Until your friend, who plays a lot of role-playing games, has just asked if your app could support different types of dice: 4-sided, 8-sided, 12-sided, 20-sided, even 100-sided...

Dice

Contributors

Over 300 content creators. Join our team.