Implementing the ItemManager class

The to-do app will show all the to-do items in a list. The list of items will be managed by a class called ItemManager. It will expose an interface to get, add, and remove items.

Open Project Navigator and select the ToDoTests group. Go to iOS | Source | Unit Test Case Class to create a test case class with the name, ItemManagerTests, and put it in the Model folder. Import the ToDo module (@testable import ToDo) and remove the two test method templates.

The requirements from Chapter 2, Planning and Structuring Your Test-Driven iOS App, ask for a list with unchecked to-do items at the top and checked to-do items at the bottom of the list in the app. How the items are presented is not a matter of concern with regard to the model. But it has to be possible to get the number of unchecked and checked to-do items from the item manager.

Add the following code to ItemManagerTests:

The sut abbreviation stands for System Under Test. We could also write this as itemManager, but using sut makes it easier to read, and it also allows us to copy and paste test code into other tests when appropriate.

The test is not yet finished, but it already fails because ItemManager is an unresolved identifier. Open Project Navigator again and select the ToDo group. Go to iOS | Source | Swift File. This will create a Swift file and let's call it ItemManager.swift, and select the Model folder as the file location.

Add the the following class definition:

This is enough to make the test code compilable. Run the tests to make sure that all the tests pass and we can continue writing them. In testToDoCount_Initially_ShouldBeZero(), add the assert function highlighted in the following code:

With this addition, the test method tests whether ItemManager has the toDoCount property and if it is initially set to zero.

But the test does not compile again because Value of type 'ItemManager' has no member called toDoCount. The simplest way to make the test pass is to add the following property declaration to ItemManager:

Run the tests. All the tests pass. The code and tests look good, so we do not need to refactor them.

In addition to the unchecked items, we also need to be able to get the number of checked items from the item manager. Add the following test to ItemManagerTests:

To make this test pass, add the following property definition to ItemManager:

Run the tests to check that this is enough to make them pass. If we look at the previously written test methods, we'll see a repetition. The sut variable is initialized in each test method. Let's refactor the test methods and remove the repetition. Add the following property declaration to the beginning of ItemManagerTests:

Then, at the end of setUp(), add this initialization of sut:

Now, we can remove it from the tests:

Run the tests again to make sure that we have not broken anything with the refactoring.

The item manager should be able to add items to the list. Therefore, it should provide a method that takes an item. Later, we can call this method from the View Controller that will provide a UI to add items. Add the following code to ItemManagerTests:

Here, we assume that ItemManager should have an addItem(_:) method. You can see how TDD helps us think about the class/struct interface before a feature is implemented.

The ItemManager class does not have an addItem(_:) method, and the test does not compile. Let's add the simplest implementation of addItem(_:):

Run the tests to make sure they all pass. Now, we need to assert that after adding an item, toDoCount is one. Add the following assert to testToDoCount_AfterAddingOneItem_IsOne():

Run the tests. The tests fail because toDoCount is a constant, and therefore, it never changes. Replace the highlighted lines in ItemManager:

We have converted the toDoCount constant to a variable and added code in addItem(_:) to increase its value.

Run the tests. Everything works. The code and tests look good and there is nothing to refactor.

Nevertheless, the code clearly does not do what we intend it to. The item passed into addItem(_:) is not used or stored at all. This is a sign that we need another test.

The to-do items need to be presented to the user somehow. Therefore, ItemManager needs to provide a method that returns an item. Add the following code to ItemManagerTests:

At this point, we have to stop writing the test because this code does not compile. There is no itemAtIndex(_:) method in ItemManager yet. We need to add it before we can continue with the test. Add the following to ItemManager:

It is the simplest implementation that makes the test code compilable again. Now, add the following assert to testItemAtIndex_ShouldReturnPreviouslyAddedItem():

The test fails because itemAtIndex(_:) returns an item with an empty title. To fix it, we need to add an array to store the item passed into addItem(_:), and use the same array to return the item again in itemAtIndex(_:). Replace the implementation of ItemManager with the following code:

Let's go through the changes step by step. We have added a toDoItems array to store the to-do items. The array is private because we want to encapsulate the underlying array. In addItems(_:), the item that's passed in is added to the array, and in itemAtIndex(_:), the item at the specified index is returned.

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

The user has to be able to check the items. The checked items need to be accessible from the item manager. Add the following code to ItemManagerTests:

This code does not compile because there is no checkItemAtIndex(_:) method in ItemManager. To make the test code compilable, add it to ItemManager:

When the user checks an item, toDoCount should decrease and doneCount should increase. Add the following asserts to testCheckingItem_ChangesCountOfToDoAndOfDoneItems():

To make this test pass, we simply decrease and increase the values. A possible implementation could look like this:

This is the simplest implementation that makes the tests pass. Again, the code clearly does not do what we have planned. When checking an item, it should be removed from the toDoItems array. We need another test to ensure that it implements this behavior:

This test fails. To make it pass, add the following line to checkItemAtIndex(_:):

This code uses the removeAtIndex(_:) method of the built-in array type. Run the tests. All the tests pass. There is nothing further to refactor.

In the app, the checked items will be shown below the unchecked items. This means that ItemManager also needs to provide a method that returns checked items. Add the following code to ItemManagerTests:

Before we can continue writing the test, we need to add doneItemAtIndex(_:) to ItemManager:

Again, this is the simplest implementation to make the test pass, so let's continue writing the test. Add the following assert to testDoneItemAtIndex_ShouldReturnPreviouslyCheckedItem():

This test fails because we return a dummy item from doneItemAtIndex(_:). To make it pass, replace the implementation of ItemManager with the following code:

We have added a doneItems array to store the checked items. In checkItemAtIndex(_:), we take the item removed from toDoItems and add it to doneItems. In doneItemAtIndex(_:), we simply return the item for the passed in index from the doneItems array.

Run the tests. All the tests pass. But there is a small thing we should refactor. The todoCount and doneCount variables are always the same as the count of the toDoItems and doneItems arrays, respectively. So, replace the todoCount and doneCount variables with computed properties:

Remove the lines with the --toDoCount, ++todoCount, and ++doneCount statements. Run the tests to make sure that everything still works.

There is something else that should be improved. To assert the equality of ToDoItem instances, we have used assert functions like this:

But we would like to write them like this:

If we try to do this and run the tests, we get this error:

To figure out what this means, let's have a look at the definition of XCTAssertEqual:

The important information here is <T : Equatable>. It indicates that we can only use XCTAsserEqual to check whether two elements are equal when they have the same type, and this type should conform to the Equatable protocol. We could stop here and decide that we do not need to make ToDoItem conform to Equatable, just to make the tests clearer. We can always compare each property of the items. But test code is still code. It should be easy to read and to understand.

In addition to this, we would like to make sure that the user cannot add the same item to the list twice because doing this does not add any value to the app. In fact, it could be considered a bug. To check whether an item is already managed by the list, we need to also be able to easily check whether two items represent the same information. This again means that to-do items need to be Equatable. In the next few sections, we will add conformance to Equatable, ToDoItem, and Location.

But before we continue, replace the assertion in the last test with the assertion we had earlier:

Run the tests again to make sure that we start from a green state.

Open ToDoItemTests.swift in the editor and ToDoItem.swift in the assistant editor. We would like to be able to compare to-do items using XCTAssertEqual. Add the following test to ToDoItemTests to drive the implementation of Equatable conformance:

The static analyzer tells us that it Cannot invoke 'XCTAssertEqual' with an argument list of type '(ToDoItem, ToDoItem)'. This is because ToDoItem is not Equatable. Make ToDoItem conform to Equatable like this:

Now, we get an error saying that ToDoItem does not conform to the Equatable protocol. The Equatable protocol looks like this for Swift 2.0:

So, we need to implement the == equivalence operator for ToDoItem. The operator needs to be defined in a global scope. At the end of ToDoItem.swift, outside of the ToDoItem class, add the following code:

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

The implementation of the equivalence operator is strange because it doesn't check any properties of the items that are passed in. But following the rules of TDD, it is good enough. Let's move on to more complicated tests:

The two items differ in terms of their location names. Run the test. It fails because the equivalence operator always returns true. But it should return false if the locations differ. Replace the implementation of the operator with this code:

Again, the static analyzer complains. This is because, this time, Location does not conform to Equatable. In fact, Location needs to be Equatable too. But before we can move to Location and its tests, we need to have all tests pass again. Replace the highlighted line in the equivalence operator to make all the tests pass again:

For now, we just test whether the names of the locations differ. Later, when Location conforms to Equatable, we will be able to compare locations directly.

Open LocationTests.swift in the editor and Location.swift in the Assistant Editor. Add the following test to LocationTests:

Again, this code does not compile because Location does not conform to Equatable. Let's add the Equatable conformance. Replace the struct declaration with this:

Add the the dummy implementation of the equivalence operator in Location.swift, but outside of the Location struct:

Run the tests. All the tests pass again, and at this point, there is nothing to refactor. Add the following test:

The two locations differ in terms of latitude. Run the test. This test fails because the equivalence operator always returns true. Replace the implementation of the equivalence operator with the following code:

In case the latitude of the location's coordinates differ, the operator returns false; otherwise, it'll return true. Run the tests. All the tests pass again. Next, we need to make sure that the locations that differ in terms of longitude are not equal. Add the following test:

Run the test. This test fails because we will not check the longitude in the equivalence operator yet. Add the highlighted lines to the operator:

Run the tests. All the tests pass again. The last two tests that we have written are very similar to each other. The only difference is in the definition of the first coordinate. Let's refactor the test code to make it clearer to read and easier to maintain. First, we create a method that performs the tests that are given different values for the Location properties:

This method takes two strings and optional tuples, respectively. With this information, it creates two Location instances and compares them using XCTAssertNotEqual.

Now, we can replace testWhenLatitudeDifferes_ShouldBeNotEqual() with this:

To check whether this test still works, we need to make it fail by removing some implementation code. If the test passes again when we re-add the code, we can be confident that the tests still works. In Location.swift, remove the check for the nonequality of latitude:

Run the test. The test does, indeed, fail but the failure shows in the line where XCTAssertNotEqual is located:

Equatable

We would like to see the failure in the test method. In Chapter 1, Your First Unit Tests, we discussed how to change the line for which the failure is reported. The easiest way to do this is to add the line argument to performNotEqualTestWithLocationProperties(…) and use it in the assertion:

func performNotEqualTestWithLocationProperties(firstName: String,
    secondName: String,
    firstCoordinate: (Double, Double),
    secondCoordinate: (Double, Double),
    line: UInt) {
        // …
        XCTAssertNotEqual(firstLocation, secondLocation, line: line)
}

In testWhenLatitudeDifferes_ShouldBeNotEqual(), we need to call this method like this:

performNotEqualTestWithLocationProperties("Home",
    secondName: "Home",
    firstCoordinate: (1.0, 0.0),
    secondCoordinate: (0.0, 0.0),
    line: 52)

The number 52 is the line number at which the method call starts in my case. This could be different for you. Run the tests again. The failure is now reported in the specified line.

But we cannot be satisfied with this solution. A hardcoded value for the line number is a bad idea. What if we want to add a test at the beginning of the class or add something to setUp()? Then, we would have to change the line argument of all the calls of that function. There has to be a better way of doing this.

C has some magic macros that are also available when writing Swift code. Replace 52 (or whatever you have put there) with the __LINE__ magic macro. Run the tests again. Now, the failure is reported in the line where the magic macro is. This is good enough even if the method call is spread over several lines.

But we can even do better by using default values for method arguments. Add a default value to the last argument of performNotEqualTestWithLocationProperties(…):

As the method now has a default value for the last argument, we can remove it from the call:

Run the tests again. The failure is now reported at the beginning of the call but without the need to hardcode the line number. Add the code again to the equivalence operator that we had to remove in order to make the test fail:

Run the tests to make sure that all of them pass again. Now, replace testWhenLongitudeDifferes_ShouldBeNotEqual() with the following code:

Run the tests. All the tests pass.

If one location has a coordinate set and the other one does not, they should be considered to be different. Add the following test to make sure that the equivalence operator works this way:

Run the tests. All the tests pass. The current implementation of the equivalence operator already works in this way.

Right now, two locations with the same coordinate but different names are equivalent. But we want them to be considered different. Add the following test:

This test fails. Add the following if condition right before the return true line in the implementation of the equivalence operator:

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

The Location struct now conforms to Equatable. Let's go back to ToDoItem and continue where we left off.

First, let's refactor the current implementation of the equivalence operator of ToDoItem. Now that Location conforms to Equatable, we can check whether the two locations are different using the != operator (which we get for free by implementing the == operator):

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

If one to-do item has a location and the other does not, they are not equal. Add the following test to ToDoItemTests to make sure this is the case:

The test already passes. Let's make sure that it also works the other way round. Change the let keywords to var, and add the following code to the end of testWhenOneLocationIsNilAndTheOtherIsnt_ShouldBeNotEqual():

Run the tests. This also works with the current implementation of the equivalence operator of ToDoItem.

Next, if the timestamp of two to-do items differs, they are different. The following code tests whether this is the case in our implementation:

Both to-do items are equivalent to each other, except for the timestamp. The test fails because we do not compare the timestamp in the equivalence operator yet. Add the following if condition in the operator implementation right before the return true statement:

Run the tests. All the tests pass and there is nothing to refactor. From the tests about the equivalence of the Location instances, we already know that this implementation is enough even if one of the timestamps is nil. So, no more tests for the equivalence of timestamps are needed.

Now, let's make sure that two to-do items that differ in their descriptions are not equal. Add this test:

Adding the following if condition to the equivalence operator right before the return true statement, makes the test pass:

The last thing we have to check is whether two to-do items differ if their titles differ. Add this test:

With all the experience we have gained in this section, the implementation nearly writes itself. Add another if condition again right before the return true statement:

Run the tests. All the tests pass.

Now that ToDoItem and Location conform to Equatable, the to-do items and locations can be used directly in XCTAssertEqual. Go through the tests and make the necessary changes.

The ItemManager class needs to provide a method to remove all items. Add the following code to ItemManagerTests:

This code adds two to-do items to the manager and checks one item. Then, it asserts that the count of the items has the expected values and calls removeAllItems().

The code does not compile because removeAllItems() is not implemented yet. Add the minimal implementation needed to make the test code compilable:

Now, add the following assertions to testRemoveAllItems_ShouldResultInCountsBeZero() to check whether the items have been removed:

To make this test pass, we need to remove all the items from the underlying arrays. Add the following implementation in removeAllItems():

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

As mentioned earlier, we would like to make sure that each to-do item can only be added to the list once. To ensure this behavior is implemented, add the following test to ItemManagerTests:

This test fails. To make the test pass, we need to check whether the item we want to add to the list is already contained in the list. Fortunately, Swift provides a method on the Array type that does exactly this. Replace addItem(_:) with the following code:

Run the tests. All the tests pass, and we are finally finished with the implementation of our model.