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

Assert functions in the XCTest framework

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

A test method without an XCTAssert function that doesn't throw an error will always pass.

The most important assert functions are listed here:

  • XCTAssertTrue(_:_:file:line:): This asserts that an expression is true.
  • XCTAssert(_:_:file:line:): This assertion is the same as XCTAssertTrue(_:_:file:line:).
  • XCTAssertFalse(_:_:file:line:): This asserts that an expression is false.
  • XCTAssertEqual(_:_:_:file:line:): This asserts that two expressions are equal.
  • XCTAssertEqual(_:_: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.
  • XCTAssertNotEqual(_:_:accuracy:_:file:line:): This asserts that two expressions are not the same, taking into account the accuracy defined in the accuracy parameter.
  • XCTAssertNil(_:_:file:line:): This asserts that an expression is nil.
  • XCTAssertNotNil(_:_:file:line:): This asserts that an expression is not nil.
  • XCTFail(_:file:line:): This always fails.

To take a look at a 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, as shown in the following screenshot:

Figure 1.5 – Jump to Definition of a selected function

Figure 1.5 – Jump to Definition of a selected function

Note that most XCTAssert functions can be replaced with XCTAssert(_:_:file:line). For example, the following assert functions are asserting the same thing:

// This assertion asserts the same as...
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 more precise assertions whenever possible, as 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(1, 2)
// Log output:
// XCTAssertEqual failed: ("1") is not equal to ("2")
XCTAssert(1 == 2)
// Log output:
// XCTAssertTrue failed

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.

Custom assert functions

But sometimes, even the more precise assert function is not precise enough. In this case, you can write your own assert functions. As an example, let's assume we have a test that asserts that two dictionaries have the same content. If we used XCTAssertEqual to test that, the log output would look like this:

func test_dictsAreQual() {
  let dict1 = ["id": "2", "name": "foo"]
  let dict2 = ["id": "2", "name": "fo"]
  XCTAssertEqual(dict1, dict2)
  // Log output:
  // XCTAssertEqual failed: ("["name": "foo", "id":
    "2"]")...
  // ...is not equal to ("["name": "fo", "id": "2"]")
}

For the short dictionaries in this example, finding the difference is quite easy. But what if the dictionary has 20 entries or even more? When we add the following assert function to the test target, we get better log outputs:

func DDHAssertEqual<A: Equatable, B: Equatable>
  (_ first: [A:B],
   _ second: [A:B]) {
  if first == second {
    return
  }
  for key in first.keys {
    if first[key] != second[key] {
      let value1 = String(describing: first[key]!)
      let value2 = String(describing: second[key]!)
      let keyValue1 = "\"\(key)\": \(value1)"
      let keyValue2 = "\"\(key)\": \(value2)"
      let message = "\(keyValue1) is not equal to
        \(keyValue2)"
      XCTFail(message)
      return
    }
  }
}

This method compares the values for each key and fails if one of the values differs. Additionally, this assert function should check whether the dictionaries have the same keys. This functionality is left as an exercise for the reader. Here, we focus this example on how to write a custom assert function. By keeping the example short, the main point is easier to understand.

When we run this test with the preceding dictionaries, we see the following output in Xcode:

Figure 1.6 – Xcode showing the failure at two different places

Figure 1.6 – Xcode showing the failure at two different places

As you can see in the preceding screenshot, Xcode shows the test failure in the assert function. In the test method, it only shows a redirect to the failure. Fortunately, there is an easy fix for that. All we have to do is to pass file and line parameters to the custom assert function and use these in the XCTFail call, like this:

 func DDHAssertEqual<A: Equatable, B: Equatable>(
  _ first: [A:B],
  _ second: [A:B],
  file: StaticString = #filePath,        // << new
  line: UInt = #line) {                  // << new
    if first == second {
      return
    }
    for key in first.keys {
      if first[key] != second[key] {
        let value1 = String(describing: first[key]!)
        let value2 = String(describing: second[key]!)
        let keyValue1 = "\"\(key)\": \(value1)"
        let keyValue2 = "\"\(key)\": \(value2)"
        let message = "\(keyValue1) is not equal to
          \(keyValue2)"
        XCTFail(message, file: file, line: line)  // << new
        return
      }
    }
  }

Note that our assert function now has two new parameters: file and line, with the default values #filePath and #line, respectively. When the function is called in a test method, these default parameters make sure that the file path and the line of the call site are passed into that assert function. These parameters are then forwarded into the XCTAssert functions (XCTFail in our case, but this works with all XCT... functions). As a result, the failure is now shown in the line in which the DDHAssertEqual function is called, and we didn't have to change the call of the assert function. The following screenshot illustrates this:

Figure 1.7 – Improved failure reporting

This example shows how easy it is to write your own assert functions that behave like the ones that come with Xcode. Custom assert functions can improve the readability of the test code, but keep in mind that this is also code you have to maintain.