Book Image

Test-Driven iOS Development with Swift - Fourth Edition

By : Dr. Dominik Hauser
Book Image

Test-Driven iOS Development with Swift - Fourth Edition

By: Dr. Dominik Hauser

Overview of this book

Test-driven development (TDD) is a proven way to find software bugs earlier on in software development. Writing tests before you code improves the structure and maintainability of your apps, and so using TDD in combination with Swift 5.5's improved syntax leaves you with no excuse for writing bad code. Developers working with iOS will be able to put their knowledge to work with this practical guide to TDD in iOS. This book will help you grasp the fundamentals and show you how to run TDD with Xcode. You'll learn how to test network code, navigate between different parts of the app, run asynchronous tests, and much more. Using practical, real-world examples, you'll begin with an overview of the TDD workflow and get to grips with unit testing concepts and code cycles. You'll then develop an entire iOS app using TDD while exploring different strategies for writing tests for models, view controllers, and networking code. Additionally, you'll explore how to test the user interface and business logic of iOS apps and even write tests for the network layer of the sample app. By the end of this TDD book, you'll be able to implement TDD methodologies comfortably in your day-to-day development for building scalable and robust applications.
Table of Contents (17 chapters)
1
Section 1 –The Basics of Test-Driven iOS Development
5
Section 2 –The Data Model
9
Section 3 –Views and View Controllers
13
Section 4 –Networking and Navigation

Building your first automatic unit test

If you have done some iOS development (or application development in general) already, the following example might seem familiar to you.

You are planning to build an app. You start collecting features, drawing some sketches, or your project manager hands the requirements to you. At some point, you start coding. You set up the project and start implementing the required features of the app.

Let's say the app has an input form, and the values the user puts in have to be validated before the data can be sent to the server. The validation checks, for example, whether the email address and the phone number have a valid format. After implementing the form, you want to check whether everything works. But before you can test it manually, you need to write code that presents the form on the screen. Then, you build and run your app in the iOS simulator. The form is somewhere deep in the view hierarchy, so you navigate to the view and put the values into the form. It doesn't work—something is wrong with the phone number validation code. You go back to the code and try to fix the problem. Sometimes, this also means starting the debugger and stepping through the code to find the bug.

Eventually, the validation works for the test data you put in. Normally, you would need to test for all possible values to make sure that the validation not only works for your name and your data, but also for all valid data. But there is this long list of requirements on your desk, and you are already running late. The navigation to the form takes three taps in the simulator and putting in all the different values just takes too long. You are a coder, after all.

If only a robot could perform this testing for you.

What are unit tests?

Automatic unit tests act like this robot for you. They execute code, but without having to navigate to the screen with the feature to test. Instead of running the app over and over again, you write tests with different input data and let the computer test your code in the blink of an eye. Let's see how this works in a simple example.

Implementing a unit test example

In this example, we write a simple function that counts the number of vowels in a string. Proceed as follows:

  1. Open Xcode and go to File | New | Project.
  2. Navigate to iOS | Application | App and click on Next.
  3. Put in the name FirstDemo, select Storyboard for the Interface field and Swift for the Language field, and check Include Tests.
  4. Uncheck Use Core Data and click on Next. The following screenshot shows the options in Xcode:
Figure 1.1 – Setting up your new project

Figure 1.1 – Setting up your new project

Xcode sets up a project ready for development, in addition to two test targets for your unit and your UI tests.

  1. Open the FirstDemoTests folder in the project navigator. Within the folder, there is one file: FirstDemoTests.swift.
  2. Select FirstDemoTests.swift to open it in the editor.

What you see here is a test case. A test case is a class comprising several tests. In the beginning, it's a good practice to have one test case for each class in the main target.

Let's go through this file step by step, as follows:

The file starts with the import of the test framework and the main target, as illustrated here:

import XCTest
@testable import FirstDemo

Every test case needs to import the XCTest framework. It defines the XCTestCase class and the test assertions that you will see later in this chapter.

The second line imports the FirstDemo module. All the code you write for the demo app will be in this module. By default, classes, structs, enums, and their methods are defined with internal access control. This means that they can be accessed only from within the module. But the test code lives outside of the module. To be able to write tests for your code, you need to import the module with the @testable keyword. This keyword makes the internal elements of the module accessible in the test case.

Next, we'll take a look at the class declaration, as follows:

class FirstDemoTests: XCTestCase {

Nothing special here. This defines the FirstDemoTests class as a subclass of XCTestCase.

The first two methods in the class are shown in the following code snippet:

override func setUpWithError() throws {
  // Put setup code here. This method ...
}
override func tearDownWithError() throws {
  // Put teardown code here. This method ...
}

The setUpWithError() method is called before the invocation of each test method in the class. Here, you can insert the code that should run before each test. You will see an example of this later in this chapter.

The opposite of setUpWithError() is tearDownWithError(). This method is called after the invocation of each test method in the class. If you need to clean up after your tests, put the necessary code in this method.

The next two methods are template tests provided by the template authors at Apple:

func testExample() throws {
  // This is an example of a functional test case.
  // Use XCTAssert and related functions to ...
}
func testPerformanceExample() throws {
  // This is an example of a performance test case.
  self.measure {
    // Put the code you want to measure the time of here.
  }
}

The first method is a normal unit test. You will use this kind of test a lot in the course of this book.

The second method is a performance test. It is used to test methods or functions that perform time-critical computations. The code you put into the measure closure is called 10 times, and the average duration is measured. Performance tests can be useful when implementing or improving complex algorithms and to make sure that their performance does not decline. We will not use performance tests in this book.

All the test methods that you write have to have the test prefix; otherwise, the test runner can't find and run them. This behavior allows easy disabling of tests—just remove the test prefix of the method name. Later, you will take a look at other possibilities to disable some tests without renaming or removing them.

Now, let's implement our first test. Let's assume that you have a method that counts the vowels of a string. A possible implementation looks like this:

func numberOfVowels(in string: String) -> Int {
  let vowels: [Character] = ["a", "e", "i", "o", "u",
                             "A", "E", "I", "O", "U"]
  var numberOfVowels = 0
  for character in string {
    if vowels.contains(character) {
      numberOfVowels += 1
    }
  }
  return numberOfVowels
}

I guess this code makes you feel uncomfortable. Please keep calm. Don't throw this book into the corner—we will make this code more "swifty" soon. Add this method to the ViewController class in ViewController.swift.

This method does the following things:

  1. First, an array of characters is defined containing all the vowels in the English alphabet.
  2. Next, we define a variable to store the number of vowels. The counting is done by looping over the characters of the string. If the current character is contained in the vowels array, numberOfVowels is increased by one.
  3. Finally, numberOfVowels is returned.

Open FirstDemoTests.swift and delete the methods with the test prefix. Then, add the following method:

func test_numberOfVowels_whenGivenDominik_shouldReturn3() {
  let viewController = ViewController()
  let result = viewController.numberOfVowels(in: "Dominik")
  XCTAssertEqual(result, 3,
    "Expected 3 vowels in 'Dominik' but got \(result)")
}

This test creates an instance of ViewController and assigns it to the viewController constant. It calls the function that we want to test and assigns the result to a constant. Finally, the code in the test method calls the XCTAssertEqual(_:, _:) function to check whether the result is what we expected. If the two first parameters in XCTAssertEqual are equal, the test passes; otherwise, it fails.

To run the tests, select a simulator of your choice and go to Product | Test, or use the U shortcut. Xcode compiles the project and runs the test. You will see something similar to this:

Figure 1.2 – Xcode shows a green diamond with a checkmark when a test passes

Figure 1.2 – Xcode shows a green diamond with a checkmark when a test passes

The green diamond with a checkmark on the left-hand side of the editor indicates that the test passed. So, that's it—your first unit test. Step back for a moment and celebrate. This could be the beginning of a new development paradigm for you.

Now that we have a fast test that proves that the numberOfVowels(in:) method does what we intended, we are going to improve the implementation. The method looks like it has been translated from Objective-C. But this is Swift. We can do better. Open ViewController.swift, and replace the numberOfVowels(in:) method with this more "swifty" implementation:

func numberOfVowels(in string: String) -> Int {
  let vowels: [Character] = ["a", "e", "i", "o", "u",
                             "A", "E", "I", "O", "U"]
  return string.reduce(0) {
    $0 + (vowels.contains($1) ? 1 : 0)
  }
}

Here, we make use of the reduce function, which is defined on the array type. The reduce function combines all the elements of a sequence into one value using the provided closure. $0 and $1 are anonymous shorthand arguments representing the current value of the combination and the next item in the sequence. Run the tests again (U) to make sure that this implementation works the same as the one earlier.

Disabling slow UI tests

You might have realized that Xcode also runs the UI test in the FirstDemoUITests target. UI tests are painfully slow. We don't want to run those tests every time we type the U shortcut. To disable the UI tests, proceed as follows:

  1. Open the scheme selection and click on Edit Scheme…, as shown in the following screenshot:
Figure 1.3 – Selecting the target selector to open the scheme editor

Figure 1.3 – Selecting the target selector to open the scheme editor

  1. Xcode opens the scheme editor. Select the Test option and uncheck the FirstDemoUITests target, as shown in the following screenshot:
Figure 1.4 – Deselecting the UI test target

Figure 1.4 – Deselecting the UI test target

This disables the UI tests for this scheme, and running tests becomes fast. Check yourself and run the tests using the U shortcut.

Before we move on, let's recap what we have seen so far. First, you learned that you could easily write code that tests your code. Secondly, you saw that a test helped improve the code because now, you don't have to worry about breaking the feature when changing the implementation.

To check whether the result of the method is as we expected, we used XCTAssertEqual(_:, _:). This is one of many XCTAssert functions that are defined in the XCTest framework. The next section shows the most important ones.