Unit testing is one of those things that we all know deep down we should be doing, but it seems too difficult, too boring, or too much like hard work.
It’s so much fun creating code that does exciting things; why would anyone want to spend half the time writing code that just checks things?
The reason is confidence! In this Unit testing on macOS tutorial, you’ll learn how to test your code and you will gain confidence that your code is doing what you want it to do, confidence that you can make major changes to your code and confidence that you won’t break anything.
Getting Started
This project uses Swift 3 and requires, at a minimum, Xcode 8 beta 6. Download the starter project and open it in Xcode.
If you have done any other tutorials here at raywenderlich.com, you are probably expecting to build and run at this stage, but not this time — you are going to test. Go to the Product menu and choose Test. Note the shortcut — Command-U — you’ll be using it a lot.
When you run the tests, Xcode will build the app and you will see the app window appear a couple of times before you get a message saying “Test Succeeded”. In the Navigator pane on the left, select Test navigator.
This shows you the three tests added by default; each one has a green tick beside it, showing that the test passed. To see the file containing those tests, click on the second line in the Test Navigator where it says High RollerTests preceded by an uppercase T icon.
There are a few important things to note here:
- The imports: XCTest is the testing framework provided by Xcode.
@testable import High_Roller
is the import that gives the testing code access to all the code in theHigh_Roller
module. Every test file will need these two imports. setup()
andtearDown()
: these are called before and after every single test method.testExample()
andtestPerformanceExample()
: actual tests. The first one tests functionality, and the second one tests performance. Every test function name must begin withtest
so that Xcode can recognize it as a test to perform.
What Is Unit Testing?
Before you get into writing your own tests, it’s time for a brief discussion about unit testing, what it actually is and why you should use it.
A unit test is a function that tests a single piece — or unit — of your code. It doesn’t get included in the code of your application, but is used during development to check that your code does what you expected.
A common first reaction to unit tests is: “Are you telling me I should write twice as much code? One function for the app itself and another to test that function?” Actually, it can be worse than that — some projects end up with more testing code than production code.
At first, this seems like a terrible waste of time and effort — but wait until a test catches something that you didn’t spot, or alerts you to a side-effect of re-factoring. That’s when you realize what an amazing tool this is. After a while, any project without unit tests feels very fragile, and you’ll hesitate to make any changes because you cannot be sure what will happen.
Test Driven Development
Test Driven Development (TDD) is a branch of unit testing where you start with the tests and only write code as required by the tests. Again, this seems like a very strange way to proceed at first and can produce some very peculiar code as you’ll see in a minute. The upshot is that this process really makes you think about the purpose of the code before coding begins.
Test Driven Development has three repeating steps:
- Red: Write a failing test.
- Green: Write the minimum code needed to make the test pass.
- Refactor: Optional; if any app or test code can be re-factored to make it better, do it now.
This sequence is important and the key to effective TDD. Fixing a failing test gives you a clear indication you know exactly what your code is doing. If your test passes the first time, without any new code being written, then you have not correctly pin-pointed the next stage of development.
To start, you’ll write a series of tests and the accompanying code using TDD.
The Test Project
This project is a dice rolling utility for board gamers. Ever sit down to play a game with the family and discover that the dog ate the dice? Now your app can come to the rescue. And if anyone says “I don’t trust a computer not to cheat!” you can proudly say that the app has been unit tested to prove that it works correctly. That’s bound to impress the family — and you will have saved games night. :]
The model for this app will have two main object types: Dice
, which will have a value
property and a method for generating a random value, and Roll
, which will be a collection of Dice
objects with methods for rolling them all, totaling the values and so on.
This first test class is for the Dice
object type.
The Dice Test Class
In Xcode go to the File Navigator and select the High RollerTests group. Select File\New\File… and choose macOS\Unit Test Case Class. Click Next and name the class DiceTests
. Make sure the language is set to Swift. Click Next and Create.
Select all the code inside the class and delete it. Add the following statement to DiceTests.swift just under the import XCTest
line:
@testable import High_Roller |
Now you can delete HighRollerTests.swift as you don’t need the default tests any longer.
The first thing to test is whether a Dice
object can be created.
Your First Test
Inside the DiceTests class, add the following test function:
func testForDice() { let _ = Dice() } |
This gives a compile error before you can even run the test: "Use of unresolved identifier 'Dice'"
. In TDD, a test that fails to compile is considered a failing test, so you have just completed step 1 of the TDD sequence.
To make this test pass with the minimum of code, go to the File Navigator and select the Model group in the main High Roller group. Use File\New\File… to create a new Swift file and name it Dice.swift.
Add the following code to the file:
struct Dice { } |
Go back to DiceTests.swift; the error will still be visible until the next build. However, you can now run the test in several different ways.
If you click the diamond in the margin beside the test function, only that single test will run. Try that now, and the diamond will turn into a green checkmark symbol, showing that the test has passed.
You can click a green symbol (or a red symbol that shows a failed test) at any time to run a test. There will now be another green symbol beside the class name. Clicking this will run all the tests in the class. At the moment, this is the same as running the single test, but that will soon change.
The final way to test your code is to run all the tests.
Press Command-U to run all the tests and then go to the Test Navigator where you will see your single test in the High RollerTests section; you may need to expand the sections to see it. The green checkmark symbols appear beside every test. If you move the mouse pointer up and down the list of tests, you will see small play buttons appear which you can use to run any test or set of tests.
In the Test Navigator, you can see that the High RollerUITests ran as well. The problem with UI Tests are that they’re slow. You want your tests to be fast as possible so that there is no drawback to testing frequently. To solve this problem, edit the scheme so that the UI Tests don’t run automatically.
Go to the scheme popup in the toolbar and select Edit scheme…. Click Test in the pane on the left and un-check High RollerUITests. Close the scheme window and run your tests again with Command-U. The UI Tests are faded out in the Test Navigator, but they can still be run manually.
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:
- The function name starts with
'test'
, and the remainder of the function name expresses what the test checks. - The test uses one of the many
XCTAssert
functions to confirm that the value isnil
. The second parameter ofXCTAssertNil()
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) } |
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:
rollCounter
specifies how many times the dice will be rolled. 100 for each expected number seems like a reasonable sample size.- 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 withguard
statements. - After each roll, you add the result to a dictionary.
- This assertion confirms that there are 6 keys in the dictionary, one for each of the expected numbers.
- The test uses a new assertion:
XCTAssertEqualWithAccuracy()
which allows inexact comparisons. SinceXCTAssertEqualWithAccuracy()
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.
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…
Modifying Existing Code
You don’t want to ruin your friend’s D&D nights, so head back to DiceTests.swift and add another test:
func testRollingTwentySidedDice() { var testDie = Dice() testDie.rollDie(numberOfSides: 20) XCTAssertNotNil(testDie.value) XCTAssertTrue(testDie.value! >= 1) XCTAssertTrue(testDie.value! <= 20) } |
The compiler complains because rollDie()
doesn’t take any parameters. Switch over to the assistant editor and in Dice.swift change the function declaration of rollDie()
to expect a numberOfSides
parameter:
mutating func rollDie(numberOfSides: Int) { |
But that will make the old test fail because they don’t supply a parameter. You could edit them all, but most dice rolls are for 6-sided dice (no need to tell your role-playing friend that). How about giving the numberOfSides
parameter a default value?
Change the rollDie(numberOfSides:)
definition to this:
mutating func rollDie(numberOfSides: Int = 6) { |
All the tests now pass, but you are in the same position as before: the tests don’t check that the 20-sided dice roll is really producing values from 1 to 20.
Time to write another test similar to testRollsAreSpreadRoughlyEvenly()
, but only for 20-sided dice.
func testTwentySidedRollsAreSpreadRoughlyEvenly() { var testDie = Dice() var rolls: [Int: Double] = [:] let rollCounter = 2000.0 for _ in 0 ..< Int(rollCounter) { testDie.rollDie(numberOfSides: 20) guard let newRoll = testDie.value else { XCTFail() return } if let existingCount = rolls[newRoll] { rolls[newRoll] = existingCount + 1 } else { rolls[newRoll] = 1 } } XCTAssertEqual(rolls.keys.count, 20) for (key, roll) in rolls { XCTAssertEqualWithAccuracy(roll, rollCounter / 20, accuracy: rollCounter / 20 * 0.3, "Dice gave \(roll) x \(key)") } } |
This test gives seven failures: the number of keys is only 6, and the distribution isn’t even. Have a look in the Issue Navigator for all the details.
You should expect this: rollDie(numberOfSides:)
isn’t using the numberOfSides
parameter yet.
Replace the 6
in the arc4random_uniform()
function call with numberOfSides
and Command-U again.
Success! All the tests pass — even the old ones that call the function you just changed.
Refactoring Tests
For the first time, you have some code worth re-factoring. testRollsAreSpreadRoughlyEvenly()
and testTwentySidedRollsAreSpreadRoughlyEvenly()
use very similar code, so you could separate that out into a private function.
Add the following extension to the end of the DiceTests.swift file, outside the class:
extension DiceTests { fileprivate func performMultipleRollTests(numberOfSides: Int = 6) { var testDie = Dice() var rolls: [Int: Double] = [:] let rollCounter = Double(numberOfSides) * 100.0 let expectedResult = rollCounter / Double(numberOfSides) let allowedAccuracy = rollCounter / Double(numberOfSides) * 0.3 for _ in 0 ..< Int(rollCounter) { testDie.rollDie(numberOfSides: numberOfSides) guard let newRoll = testDie.value else { XCTFail() return } if let existingCount = rolls[newRoll] { rolls[newRoll] = existingCount + 1 } else { rolls[newRoll] = 1 } } XCTAssertEqual(rolls.keys.count, numberOfSides) for (key, roll) in rolls { XCTAssertEqualWithAccuracy(roll, expectedResult, accuracy: allowedAccuracy, "Dice gave \(roll) x \(key)") } } } |
This function name doesn’t start with test
, as it’s never run on its own as a test.
Go back to the main DiceTests
class and replace testRollsAreSpreadRoughlyEvenly()
and testTwentySidedRollsAreSpreadRoughlyEvenly()
with the following:
func testRollsAreSpreadRoughlyEvenly() { performMultipleRollTests() } func testTwentySidedRollsAreSpreadRoughlyEvenly() { performMultipleRollTests(numberOfSides: 20) } |
Run all the tests again to confirm that this works.
Using #line
To demonstrate another useful testing technique, go back to Dice.swift and undo the 20-sided dice change you made to rollDie(numberOfSides:)
: replace numberOfSides
with 6
inside the arc4random_uniform()
call. Now run the tests again.
testTwentySidedRollsAreSpreadRoughlyEvenly()
has failed, but the failure messages are in performMultipleRollTests(numberOfSides:)
— not a terribly useful spot.
Xcode can solve this for you. When defining a helper function, you can supply a parameter with a special default value — #line
— that contains the line number of the calling function. This line number can be used in the XCTAssert
function to send the error somewhere useful.
In the DiceTests
extension, change the function definition of performMultipleRollTests(numberOfSides:)
to the following:
fileprivate func performMultipleRollTests(numberOfSides: Int = 6, line: UInt = #line) { |
And change the XCTAsserts
like this:
XCTAssertEqual(rolls.keys.count, numberOfSides, line: line) for (key, roll) in rolls { XCTAssertEqualWithAccuracy(roll, expectedResult, accuracy: allowedAccuracy, "Dice gave \(roll) x \(key)", line: line) } |
You don’t have to change the code that calls performMultipleRollTests(numberOfSides:line:)
because the new parameter is filled in by default. Run the tests again, and you’ll see the error markers are on the line that calls performMultipleRollTests(numberOfSides:line:)
— not inside the helper function.
Change rollDie(numberOfSides:)
back again by putting numberOfSides
in the arc4random_uniform()
call, and Command-U to confirm that everything works.
Pat yourself on the back — you’ve learned how to use TDD to develop a fully-tested model class.
Adding Unit Tests to Existing Code
TDD can be great when developing new code, but often you’ll have to retrofit tests into existing code that you didn’t write. The process is much the same, except that you’re writing tests to confirm that existing code works as expected.
To learn how to do this, you’ll add tests for the Roll
struct. In this app, the Roll
contains an array of Dice
and a numberOfSides
property. It handles rolling all the dice as well as totaling the result.
Back in the File Navigator, select Roll.swift
. Delete all the placeholder code and replace it with the following code:
struct Roll { var dice: [Dice] = [] var numberOfSides = 6 mutating func changeNumberOfDice(newDiceCount: Int) { dice = [] for _ in 0 ..< newDiceCount { dice.append(Dice()) } } var allDiceValues: [Int] { return dice.flatMap { $0.value} } mutating func rollAll() { for index in 0 ..< dice.count { dice[index].rollDie(numberOfSides: numberOfSides) } } mutating func changeValueForDie(at diceIndex: Int, to newValue: Int) { if diceIndex < dice.count { dice[diceIndex].value = newValue } } func totalForDice() -> Int { let total = dice .flatMap { $0.value } .reduce(0) { $0 - $1 } return total } } |
(Did you spot the error? Ignore it for now; that’s what the tests are for. :])
Select the High RollerTests group in the File Navigator and use File\New\File… to add a new macOS\Unit Test Case Class called RollTests
. Delete all the code inside the test class.
Add the following import to RollTests.swift:
@testable import High_Roller |
Open Roll.swift in the assistant editor, and you’ll be ready to write more tests.
First, you want to test that a Roll
can be created, and that it can have Dice
added to its dice
array. Arbitrarily, the test uses five dice.
Add this test to RollTests.swift:
func testCreatingRollOfDice() { var roll = Roll() for _ in 0 ..< 5 { roll.dice.append(Dice()) } XCTAssertNotNil(roll) XCTAssertEqual(roll.dice.count, 5) } |
Run the tests; so far, so good — the first test passes. Unlike TDD, a failing test is not an essential first step in a retrofit as the code should (theoretically) already work properly.
Next, use the following test to check that the total is zero before the dice are rolled:
func testTotalForDiceBeforeRolling_ShouldBeZero() { var roll = Roll() for _ in 0 ..< 5 { roll.dice.append(Dice()) } let total = roll.totalForDice() XCTAssertEqual(total, 0) } |
Again this succeeds, but it looks like there is some refactoring to be done. The first section of each test sets up a Roll
object and populates it with five dice. If this was moved to setup()
it would happen before every test.
Not only that, but Roll
has a method of its own for changing the number of Dice
in the array, so the tests might as well use and test that.
Replace the contents of the RollTests
class with this:
var roll: Roll! override func setUp() { super.setUp() roll = Roll() roll.changeNumberOfDice(newDiceCount: 5) } func testCreatingRollOfDice() { XCTAssertNotNil(roll) XCTAssertEqual(roll.dice.count, 5) } func testTotalForDiceBeforeRolling_ShouldBeZero() { let total = roll.totalForDice() XCTAssertEqual(total, 0) } |
As always, run the tests again to check that everything still works.
With five 6-sided dice, the minimum total is 5 and the maximum is 30, so add the following test to check that the total falls between those limits:
func testTotalForDiceAfterRolling_ShouldBeBetween5And30() { roll.rollAll() let total = roll.totalForDice() XCTAssertGreaterThanOrEqual(total, 5) XCTAssertLessThanOrEqual(total, 30) } |
Run this test — it fails! It looks like the tests have discovered a bug in the code. The problem must be in either rollAll()
or totalForDice()
, since those are the only two functions called by this test. If rollAll()
was failing, the total would be zero. However, returned total is a negative number, so have a look at totalForDice()
instead.
There’s the problem: reduce
is subtracting instead of adding the values. Change the minus sign to a plus sign:
func totalForDice() -> Int { let total = dice .flatMap { $0.value } // .reduce(0) { $0 - $1 } // bug line .reduce(0) { $0 + $1 } // fixed return total } |
Run your tests again — everything should run perfectly.
Where to Go From Here?
You can download the sample project here with all the tests from this part of the tutorial in it.
Carry on to the second half of this Unit testing on macOS tutorial for more goodness, including interface testing, network testing, performance testing and code coverage. Hope to see you there! :]
If you have any questions or comments about this part of the tutorial, please join the discussion below!
The post Unit Testing on macOS: Part 1/2 appeared first on Ray Wenderlich.