Book Image

Test-Driven iOS Development with Swift 4 - Third Edition

By : Dr. Dominik Hauser
Book Image

Test-Driven iOS Development with Swift 4 - Third Edition

By: Dr. Dominik Hauser

Overview of this book

Test-driven development (TDD) is a proven way to find software bugs early. Writing tests before you code improves the structure and maintainability of your apps. Using TDD, in combination with Swift 4's improved syntax, means there is no longer any excuse for writing bad code. This book will help you understand the process of TDD and how to apply it to your apps written in Swift. Through practical, real-world examples, you’ll learn how to implement TDD in context. You will begin with an overview of the TDD workflow and then delve into unit-testing concepts and code cycles. You will also plan and structure your test-driven iOS app, and write tests to drive the development of view controllers and helper classes. Next, you’ll learn how to write tests for network code and explore how the test-driven approach—in combination with stubs—helps you write network code even before the backend component is finished. Finally, the book will guide you through the next steps to becoming a testing expert by discussing integration tests, Behavior Driven Development (BDD), open source testing frameworks, and UI Tests (introduced in Xcode 9).
Table of Contents (9 chapters)

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. After you have set up the project, you start implementing the required features of the app.

Let's say an app is 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 looks like it's supposed to and the phone number has a valid format. You implement the form and check whether everything works. But before you can test, 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 this view and put the values into the form. It doesn't work. Next, you go back to the code and try to fix the problem. Sometimes, this also means that you need to run the debugger, and build and run to check whether the code still has errors.

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 the need of navigating 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

Open Xcode and go to File | New | Project. Navigate to iOS | Application | Single View App, and click on Next. Put in the name FirstDemo, select the language Swift, and check Include Unit Tests. Uncheck Use Core Data and Include UI Tests, and click on Next. The following screenshot shows the options in Xcode:

Xcode sets up a project ready for development in addition to a test target for your unit tests. Open the FirstDemoTests folder in the Project Navigator. Within the folder, there are two files: FirstDemoTests.swift and Info.plist. 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 good practice to have a test case for each class in the main target.

Let's go through this file step by step:

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 module FirstDemo. All the code you write for the app will be in this module. By default, classes, structs, enums, and their methods are defined as internal. This means that they can be accessed 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 to the test case.

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

class FirstDemoTests: XCTestCase { 

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

The first two methods in the class are as follows:

  override func setUp() { 
    super.setUp() 
    // Put setup code here. This method is called ... 
  } 
   

override func tearDown() { // Put teardown code here. This method is called ... super.tearDown() }

The setUp() 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 setUp() is tearDown(). 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.

There are two test methods in the template provided by Apple:

  func testExample() { 
    // This is an example of a functional test case. 
    // Use XCTAssert and related functions to verify your ... 
  } 
   

func testPerformanceExample() { // 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 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
}

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.
Without the [Character] type declaration right after the name of the constant, this would be created as an array of strings, but we need an array of characters here.
  1. 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.
  2. Finally, numberOfVowels is returned.

Open FirstDemoTests.swift methods (the methods with the test prefix). Add the following method to it:

func test_NumberOfVowels_WhenPassedDominik_ReturnsThree() { 
  let viewController = ViewController() 
   

let string = "Dominik"

let numberOfVowels = viewController.numberOfVowels(in: string)

XCTAssertEqual(numberOfVowels, 3, "should find 3 vowels in Dominik") }
Downloading the example code
You can download the example code files for all Packt books you have purchased from your account at http://www.packtpub.com. If you have purchased this book from elsewhere, you can visit http://www.packtpub.com/support and register to have the files emailed directly to you.

This test creates an instance of ViewController and assigns it to the viewController constant. It defines a string to use in the test. Then, it calls the function that we want to test and assigns the result to a constant. Finally, the test method calls the XCTAssertEqual(_, _) function to check whether the result is what we expected.

To run the tests, go to Product | Test, or use the command + U shortcut. Xcode compiles the project and runs the test. You will see something similar to what is shown in this screenshot:

The green diamond with a checkmark on the left-hand side of the editor indicates that the test passed. So, this is it. This is 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 test that proves that the 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 swifter implementation:

func numberOfVowels(in string: String) -> Int { 
  let vowels: [Character] = ["a", "e", "i", "o", "u", 
                             "A", "E", "I", "O", "U"] 
   

return string.characters.reduce(0) { $0 + (vowels.contains($1) ? 1 : 0) } }

Here, we make use of the reduce function, which is defined in the array type. Run the tests again (command + U), to make sure that this implementation works the same as the one earlier.

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.

Important built-in assert functions

Each test needs to assert some expected behavior. The use of the XCTAssert functions tells Xcode what is expected.

A test method without a XCTAssert function will always pass as long as it compiles.

The most important assert functions are:

  • XCTAssertTrue(_:_:file:line:): This asserts that an expression is true
  • XCTAssertFalse(_:_:file:line:): This asserts that an expression is false
  • XCTAssertEqual(_:_:_:file:line:): This asserts that two expressions are equal
  • XCTAssertEqualWithAccuracy(_:_:accuracy:_:file:line:): This asserts that two expressions are the same, taking into account the accuracy defined in the accuracy parameter
  • XCTAssertNotEqual(_:_:_:file:line:): This asserts that two expressions are not equal
  • XCTAssertNil(_:_:file:line:): This asserts that an expression is a nil
  • XCTAssertNotNil(_:_:file:line:): This asserts that an expression is not nil
  • XCTFail(_:file:line:): This always fails
To take a look at the full list of the available XCTAssert functions, press Ctrl, and click on the XCTAssertEqual word in the test that you have just written. Then, select Jump to Definition in the pop-up menu.

Note that all the XCTAssert functions could be written using XCTAssertTrue(_:_:file:line:). For example, these two lines of code are equivalent to each other:

// This assertion is equivalent to... 
XCTAssertEqual(2, 1+1, "2 should be the same as 1+1") 

// ...this assertion XCTAssertTrue(2 == 1+1, "2 should be the same as 1+1")

But you should use the more precise assertions whenever possible. The reason is, the log output of the more precise assertion methods tells you exactly what happened in case of a failure. For example, look at the log output of the following two assertions:

XCTAssertEqual(2, 1, "foo")
// Output:
// XCTAssertEqual failed: ("2") is not equal to ("1") - foo


XCTAssertTrue
(2 == 1, "bar")
// Output:
// XCTAssertTrue failed - bar

In the first case, you don't need to look at the test to understand what happened. The log tells you exactly what went wrong.

In all the XCTAssert functions, the last three parameters are optional. To take a look at an example for the use of all the parameters, let's check out what a failing test looks like in Xcode. Open FirstDemoTests.swift, and change the expected number of vowels from 3 to 4:

XCTAssertEqual(numberOfVowels, 4, 
               "should find 4 vowels in Dominik") 

Now, run the tests. The test fails. You will see something like this:

Xcode tells you that something went wrong with this test. Next, to the test function in the preceding screenshot, there is a red diamond with an x on line number 24. The same symbol is in the line that actually failed. On the right is the explanation of what actually went wrong, followed by the string you provided in the XCTAssertEqual function. In this case, the first parameter, numberOfVowels, is 3; and the second parameter is 4. As 3 is not equal to 4, the test fails.

As mentioned earlier, XCTAssertEqual(...) has two more parameters--file and line. These parameters allow you to alter what is printed in the debug console in case of a test failure. Navigate to View | Debug Area | Activate Console and open the debug console. If the debug area is split in half, click on the second right-most button in the bottom-right corner to hide the variables' view:

We have only one test at the moment, and the debug output is already kind of messy. Later in this chapter, we will learn that there is a better UI for the same information in Xcode.

There is one line in the output that shows the failing test:

/Users/dom/Documents/TDD_book/edition_03/code/FirstDemo/FirstDemoTests/FirstDemoTests.swift:31: error: -[FirstDemoTests.FirstDemoTests test_NumberOfVowels_WhenPassedDominik_ReturnsThree] : XCTAssertEqual failed: ("3") is not equal to ("4") - should find 4 vowels in Dominik

The output starts with the file and line where the failing tests are located. With the file and line parameter of the XCTAssert functions, we can alter what is printed there. Go back to the test method, and replace the assertion with this:

XCTAssertEqual(numberOfVowels, 4, 
               "should find 4 vowels in Dominik", 
               file: "FirstDemoTests.swift", line: 24) 

The test method starts at line number 24.

With this change, the output is as follows:

FirstDemoTests.swift:24: error: -[FirstDemoTests.FirstDemoTests test_NumberOfVowels_WhenPassedDominik_ReturnsThree] : XCTAssertEqual failed: ("3") is not equal to ("4") - should find 4 vowels in Dominik

The debug output of the test now shows the filename and line number that we specified in the assertion function. This doesn't sound like a useful feature, but later in the book, you will see an example where this really shines.

As I mentioned earlier, in all the XCTAssert functions, the last three parameters are optional. In cases where you don't need the message because the used assertion function makes clear what the failure is, you can omit it.

Before we move on with the introduction to TDD, change the test so that it passes again.