TDD in Xcode

In 1998, the Swiss company Sen:te developed OCUnit, a testing framework for Objective-C (hence, the OC prefix). OCUnit was a port of SUnit, a testing framework that Kent Beck had written for Smalltalk in 1994.

With Xcode 2.1, Apple added OCUnit to Xcode. One reason for this step was that they used it to develop Core Data at the same time that they developed Tiger, the OS with which Core Data was shipped. Bill Bumgarner, an Apple engineer, wrote this later in a blog post:

"Core Data 1.0 is not perfect, but it is a rock solid product that I'm damned proud of. The quality and performance achieved could not have been done without the use of unit testing. Furthermore, we were able to perform highly disruptive operations to the codebase very late in the development cycle. The end result was a vast increase in performance, a much cleaner code base, and rock solid release."

Apple realized how valuable unit tests can be when developing complex systems in a changing environment. They wanted third-party developers to benefit from unit tests as well. OCUnit could be (and has been) added to Xcode by hand before version 2.1. But by including it into the IDE, the investment in time that was needed to start unit testing was reduced a lot, and as a result, more people started to write tests.

In 2008, OCUnit was integrated into the iPhone SDK 2.2 to allow unit testing of iPhone apps. Four years later, OCUnit was renamed XCUnit (XC stands for Xcode).

Finally, in 2013, unit testing became a first class citizen in Xcode 5 with the introduction of XCTest. With XCTest, Apple added specific user interface elements to Xcode that helped with testing, which allowed the running of specific tests, finding failing tests quickly, and getting an overview of all the tests. We will go over the testing user interface in Xcode later in this chapter. But, first, we will take a look at TDD using Xcode in action.

For this TDD example, we are going to use the same project we created at the beginning of this chapter. Open the FirstDemo project in Xcode, and run the tests by hitting command + U. The one existing test should pass.

Let's say we are building an app for a blogging platform. When writing a new post, the user puts in a headline for the post. All the words in the headline should start with an uppercase letter.

To start the TDD workflow, we need a failing test. The following questions need to be considered when writing the test:

For the example of our blogging app, here are some possible answers for these questions:

This is enough to get us started. Enter the Red step.

Open FirstDemoTests.swift, and add the following code to the FirstDemoTests class:

This isn't a complete test method yet because we aren't really testing anything. The assertion is missing. But we have to stop writing the test at this point because the compiler complains that 'ViewController' does not have a member named `makeHeadline`.

Following the TDD workflow, we need to add code until the compiler stops printing errors. Remember 'code does not compile' within a test, means 'the test is failing'. And a failing test means we need to write code until the test does not fail anymore.

Open ViewController.swift, and add the following method to the ViewController class:

The error still remains. The reason for this is that we need to compile to make the test target aware of this change. Run the tests to check whether this change is enough to make the test green again. We get a warning that the headline constant isn't used, and we should change it to _. So, let's use it. Add the following assert function at the end of the test:

This results in another compiler error:

The reason for this error is that the makeHeadline(_:) method at the moment returns Void or (). But XCTAssertEqual can only be used if both expressions are of the same type. This makes sense as two expressions of different types can't be equal to each other.

Go back to ViewController, and change makeHeadline(_:) to this:

Look at the two tests you have for this feature. They are hard to read. The relevant information for the tests is kind of unstructured. We are going to clean it up.

Replace the two tests with the following code:

Now, the tests are easy to read and understand. They follow a logical structure: precondition, invocation, and assertion.

Run the tests. All the tests should still pass. But how do we know whether the tests still test the same thing as they did earlier? In most cases, the changes we'll make while refactoring the tests don't need to be tested themselves. But, sometimes (like in this case), it is good to make sure that the tests still work. This means that we need a failing test again. Go to makeHeadline(_:) and comment out (by adding // at the beginning) the line:

Run the tests again. Eureka! Both tests fail.

Remove the comment symbols again to make the test pass again. Now, we need to refactor the implementation code. The implementation we have right now looks like it was translated from Objective-C to Swift (if you haven't used Objective-C yet, you have to trust me on this). But Swift is different and has many concepts that make it possible to write less code that is easier to read. Let's make the implementation more swiftly. Replace makeHeadline(_:) with the following code:

In this implementation, we use the function map to iterate the words array and return another array containing the same words but starting with uppercase letters. The result is then transformed into a string by joining the words using a space as the separator.

Run the tests again to make sure we didn't break anything with the refactoring. All the tests should still pass.

With Xcode 5 and the introduction of XCTest, unit testing became tightly integrated into Xcode. Apple added many UI elements to navigate to tests, run specific tests, and find information about failing tests. One key element here is the Test Navigator.

Xcode also has a test overview where all the results of the tests are collected in one place. To open it, select the Result Navigator in the navigator panel, and select the last test in the list:

Tests overview

You can also select other tests in the list if you want to compare test runs with each other. In the editor on the right-hand side, an overview of all the tests from the selected test run are shown:

Tests overview

When you hover over one of the tests with the mouse pointer, a circle with an arrow to the right appears. If you click on the arrow, Xcode opens the test in the editor.

In the overview, there is also a Logs tab. It shows all the tests in a tree-like structure. Here is an example of what this looks like for one passing and two failing tests:

Tests overview

The logs show the test cases (in this example, one test case), the tests within the test cases (in this example, two failing and one passing test), and in addition to this, the time each test case and even each test needs to execute.

In TDD, it is important that the tests execute fast. You want to be able to execute the whole test suite in less than a second. Otherwise, the whole workflow is dominated by test execution and testing can distract your focus and concentration. You should never be tempted to switch to another application (such as Safari) because the tests will take half a minute.

If you notice that the test suite takes too long to be practical, open the logs and search for the tests that slow down testing, and try to make the tests faster. Later in the book, we will discuss strategies to speed up test execution.

Xcode provides many different ways to execute tests. You have already seen two ways to execute all the tests in the test suite: go to the Project | Test menu item, and use the command + U keyboard shortcut.

Sometimes, but usually rarely, you may need to debug your tests. As with normal code, you can set breakpoints in test code. The debugger then stops the execution of the code at a breakpoint. You can also set breakpoints in code that is to be tested to check whether you have missed something or if the code you'd like to test is actually executed.

To get a feeling of how this works, let's add an error to a test in the preceding example and debug it. Open FirstDemoTests.swift, and replace the testMakeHeadline_ReturnsStringWithEachWordStartCapital2() test method with this code:

Have you seen the error we have introduced? The value of the string expectedHeadline has a typo. The letter "s" in iS is an uppercase letter, and the letter "i" is a lowercase letter. Run the tests. The test fails and Xcode tells you what the problem is. But for the sake of this exercise, let's set a breakpoint in the line with the XCTAssertEqual() function. Click on the area on the left-hand side of the line where you want to set a breakpoint. You have to click on the area next to the red diamond. As a result, your editor will look similar to what is shown here:

Debugging tests

Run the tests again. The execution of the tests stops at the breakpoint. Open the debug console if it is not already open (go to View | Debug Area | Activate Console). In the console, some test output is visible. The last line starts with (lldb) and a blinking cursor. Put in po expectedHeadline and hit return. The term po in the code indicates print object. The console prints the value of expectedHeadline:

Now, print the value of result:

So, with the help of the debugger, you can find out what is happening.

For now, keep the typo in expectedHeadline as it is, but remove the breakpoint by dragging it with the mouse from the area to the left of the editor.