Chapter 3. A Test-Driven Data Model

iOS apps are often developed using a design pattern called Model-View-Controller (MVC). In this pattern, each class (or struct or enum) is either a model object, view, or a controller. Model objects are responsible for storing data. They should be independent from the kind of presentation by the UI. For example, it should be possible to use the same model object for an iOS app and a command-line tool on Mac.

View objects are the presenters of the data. They are responsible for making the objects visible (or hearable in the case of a VoiceOver-enabled app) for the user. Views are special for the device that the app is executed on. In the case of a cross-platform application, view objects cannot be shared. Each platform needs its own implementation of a view layer.

Controller objects communicate between the model and view objects. They are responsible for making the model objects presentable.

We will use MVC for our to-do app because it is one of the easiest design patterns, and it is commonly used by Apple in its sample code.

This chapter starts our journey in the field of TDD of the model layer of our application. It is divided in to three sections:

  • Implementing the ToDoItem struct
  • Implementing the Location struct
  • Implementing the ItemManager class

Implementing the ToDoItem struct

A to-do app needs a model class/struct to store information for to-do items.

We start by adding a new test case to the test target. Open the To-Do project that we have created in the Getting Started with Xcode section of Chapter 2, Planning and Structuring Your Test-Driven iOS App, and select the ToDoTests group. Go to File | New | File..., navigate to iOS | Source | Unit Test Case Class, and click on Next. Put in the name ToDoItemTests, make it a subclass of XCTestCase, select Swift as the language, and click on Next. In the next window, create a new folder, called Model, and click on Create.

Now, delete the ToDoTests.swift template test case.

At the time of writing this chapter, if you delete ToDoTests.swift before you add the first test case in a test target, you will see a pop-up from Xcode telling you that adding the Swift file will create a mixed Swift and Objective-C target:

Implementing the ToDoItem struct

Tip

This is a bug in Xcode 7.0. It seems that when you add the first Swift file to a target, Xcode assumes that there already have to be Objective-C files. Click on Don't Create if this happens to you because we will not use Objective-C in our tests.

Adding a title property

Open ToDoItemTests.swift and add the following import expression right below import XCTest:

@testable import ToDo

This is needed in order to be able to test the ToDo module. The @testable keyword makes the internal methods of the ToDo module accessible by the test case.

Remove the two template test methods—testExample() and testPerformanceExample().

The title of a to-do item is required. Let's write a test to ensure that an initializer exists that will take a title string. Add the following test method to the end of the test case (but within the ToDoItemTests class):

func testInit_ShouldTakeTitle() {
    ToDoItem(title: "Test title")
}

The static analyzer built into Xcode will give you a Use of unresolved identifier 'ToDoItem' complaint:

Adding a title property

We cannot compile this code because Xcode cannot find the ToDoItem identifier. Remember that a not compiling test is a failing test, and as soon as we have a failing test, we need to write implementation code to make the test pass.

To add a file for the implementation code, first click on the ToDo group in the Project Navigator. Otherwise, the added file will be put into the test group. Go to File | New | File..., navigate to iOS | Source | Swift File template, and click on Next. Create a new folder called Model. In the Save As field, add the name ToDoItem.swift, make sure that the file is added to the ToDo target and not to the ToDoTests target, and click on Create.

Open ToDoItem.swift in the editor and add the following code:

struct ToDoItem {
}

This code is a complete implementation of a struct named ToDoItem. So, Xcode should now be able to find the ToDoItem identifier. Run the test by either going to Product | Test or using the command + U shortcut. The code does not compile because there is an Extra argument 'title' in call. This means that at this stage, we could initialize an instance of ToDoItem like this:

let item = ToDoItem()

But we want to have an initializer that takes a title. We need to add a property, named title, of the String type to store the title:

struct ToDoItem {
    let title: String
}

Run the test again. It should pass. We have implemented the first micro feature of our to-do app using TDD. And it wasn't even hard. For the rest of the book, we will do this over and over again until the app is finished. But we first need to check whether there is anything to refactor in the existing test and implementation code. The tests and code are clean and simple. There is nothing to refactor yet.

Tip

Always remember to check whether refactoring is needed after you have made the tests green.

But there are a few things to note about the test. First, Xcode shows a Result of initializer is unused warning. To make this warning go away, assign the result of the initializer to an underscore: _ = ToDoItem(title: "Test title"). This tells Xcode that we know what we are doing. We want to call the initializer of ToDoItem, but we do not care about its return value.

Second, there is no XCTAssert function call in the test. To add an assert, we could rewrite the test like this:

func testInit_ShouldTakeTitle() {
    let item = ToDoItem(title: "Test title")
    XCTAssertNotNil(item, "item should not be nil")
}

But, in Swift, a nonfailable initializer cannot return nil. It always returns a valid instance. This means that the XCTAssertNotNil() method is useless. We do not need it to ensure that we have written enough code to implement the tested micro feature. Following the rules of TDD mentioned in Chapter 1, Your First Unit Tests, we are not allowed to write that code. It is not needed to drive the development, and it does not make the code better. In the following tests, we will omit the XCTAssert functions when they are not needed to make a test fail.

Before we proceed with the next few tests, let's set up the editor in a way that makes the TDD workflow easier and faster. Open ToDoItemTests.swift in the editor. Open Project Navigator, and hold down the option key while clicking on ToDoItem.swift in the navigator to open it in the Assistant Editor. Depending on the size of your screen and your preferences, you might prefer to hide the navigator again. With this setup, you have the tests and the code side by side, and switching from test to code and vice versa takes no time. In addition to this, as the relevant test is visible while you write the code, it can guide the implementation.

Adding an itemDescription property

A to-do item can have a description. We would like to have an initializer that also takes a description string. To drive the implementation, we need a failing test for the existence of this initializer:

func testInit_ShouldTakeTitleAndDescription() {
    _ = ToDoItem(title: "Test title", 
        itemDescription: "Test description")
}

Again, this code does not compile because there is Extra argument 'itemDescription' in call. To make this test pass, we add an itemDescription property of the String? type to ToDoItem:

struct ToDoItem {
    let title: String
    let itemDescription: String?
}

Run the tests. The testInit_ShouldTakeTitle() test fails (that is, it does not compile) because there is Missing argument for parameter 'itemDescription' in call. The reason for this is that we use a feature of Swift where structs have an automatic initializer with arguments defining their properties. The initializer in the first test only has one argument and, therefore, the test fails. To make the two tests pass again, replace the initializer in testInit_ShouldTakeTitle() with this:

ToDoItem(title: "Test title", itemDescription: nil)

Run the tests to ensure that all the tests pass again. But, now, the initializer in the first test looks bad. We would like to be able to have a short initializer with only one argument in case the to-do item only has a title. So, the code needs refactoring. To have more control over the initialization, we have to implement it ourselves. Add the following code to ToDoItem:

init(title: String, itemDescription: String? = nil) {
    self.title = title
    self.itemDescription = itemDescription
}

This initializer has two arguments. The second argument has a default value, so we do not need to provide both arguments. When the second argument is omitted, the default value is used.

Before we refactor the tests, run them to make sure that they still pass. Then, remove the second argument from the initializer in testInit_ShouldTakeTitle():

func testInit_ShouldTakeTitle() {
    _ = ToDoItem(title: "Test title")
}

Run the tests again to make sure that everything still works.

Removing a hidden source of bugs

To be able to use a short initializer, we need to define it ourselves. But this also introduces a new source of potential bugs. We can remove the two micro features we have implemented and still have both tests pass. To take a look at how this works, open ToDoItem.swift, and comment out the properties and assignment in the initializer:

struct ToDoItem {
    //let title: String
    //let itemDescription: String?
    
    init(title: String, itemDescription: String? = nil) {
        
        //self.title = title
        //self.itemDescription = itemDescription
    }
}

Run the tests. Both the tests still pass. The reason for this is that they do not check whether the values of the initializer arguments are actually set to any ToDoItem properties. We can easily extend the tests to make sure that the values are set. First, let's change the name of the first test to testInit_ShouldSetTitle(), and replace its contents with the following code:

let item = ToDoItem(title: "Test title")
XCTAssertEqual(item.title, "Test title", 
    "Initializer should set the item title")

This test does not compile because ToDoItem does not have a property title (it is commented out). This shows us that the test is now testing our intention. Remove the comment signs for the title property and assignment of the title in the initializer, and run the tests again. All the tests pass. Now, replace the second test with this one:

func testInit_ShouldSetTitleAndDescription() {
    let item = ToDoItem(title: "Test title", 
        itemDescription: "Test description")

    XCTAssertEqual(item.itemDescription , "Test description", 
        "Initializer should set the item description")
}

Remove the remaining comment signs in ToDoItem, and run the tests again. Both the tests pass again, and they now actually test that the initializer works.

Adding a timestamp property

A to-do item can also have a due date, represented by a timestamp. Add the following test to make sure we can initialize a to-do item with a title, description, and a timestamp:

func testInit_ShouldSetTitleAndDescriptionAndTimestamp() {
    let item = ToDoItem(title: "Test title",
        itemDescription: "Test description",
        timestamp: 0.0)

    XCTAssertEqual(0.0, item.timestamp, 
        "Initializer should set the timestamp")
}

Again, this test does not compile because there is an extra argument in the initializer. From the implementation of the other properties, we know that we have to add a timestamp property in ToDoItem and set it in the initializer:

struct ToDoItem {
    let title: String
    let itemDescription: String?
    let timestamp: Double?
    
    init(title: String, 
        itemDescription: String? = nil, 
        timestamp: Double? = nil) {
        
            self.title = title
            self.itemDescription = itemDescription
            self.timestamp = timestamp
    }
}

Run the tests. All the tests pass. The tests are green and there is nothing to refactor.

Adding a location property

The last property that we would like to be able to set in the initializer of ToDoItem is its location. The location has a name and can, optionally, have a coordinate. We will use a struct to encapsulate this data into its own type. Add the following code to ToDoItemTests:

func testInit_ShouldSetTitleAndDescriptionAndTimestampAndLocation() {
    let location = Location(name: "Test name")
}

The test is not finished, but it already fails because Location is an unresolved identifier. There is no class, struct, or enum named Location yet. Open Project Navigator, add a Swift File with the name Location.swift, and add it to the Model folder. From our experience with the ToDoItem struct, we already know what is needed to make the test green. Add the following code to Location.swift:

struct Location {
    let name: String
}

This defines a struct Location with a name property and makes the test code compliable again. But the test is not finished yet. Add the following code to testInit_ShouldSetTitleAndDescriptionAndTimestampAndLocation():

func testInit_ShouldTakeTitleAndDescriptionAndTimestampAndLocation() {
    let location = Location(name: "Test name")
    let item = ToDoItem(title: "Test title",
        itemDescription: "Test description",
        timestamp: 0.0,
        location: location)

    XCTAssertEqual(location.name, item.location?.name, 
        "Initializer should set the location")
}

Unfortunately, we cannot use the location itself yet to check for equality, so the following assert does not work:

XCTAssertEqual(location, item.location, 
    "Initializer should set the location")

The reason for this is that the first two arguments of XCTAssertEqual() have to conform to the Equatable protocol. We will add the protocol conformance later in this chapter.

Again, this does not compile because the initializer of ToDoItem does not have an argument called location. Add the location property and initializer argument to ToDoItem. The result should look like this:

struct ToDoItem {
    let title: String
    let itemDescription: String?
    let timestamp: Double?
    let location: Location?
    
    init(title: String,
        itemDescription: String? = nil,
        timestamp: Double? = nil,
        location: Location? = nil) {
        
            self.title = title
            self.itemDescription = itemDescription
            self.timestamp = timestamp
            self.location = location
    }
}

Run the tests again. All the tests pass and there is nothing to refactor.

We have now implemented a struct to hold the to-do items using TDD.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.59.237.58