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
:
func testToDoCount_Initially_ShouldBeZero() { let sut = ItemManager() }
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:
class ItemManager { }
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:
func testToDoCount_Initially_ShouldBeZero() { let sut = ItemManager() XCTAssertEqual(sut.toDoCount, 0, "Initially toDo count should be 0") }
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
:
let toDoCount = 0
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
:
func testDoneCount_Initially_ShouldBeZero() { let sut = ItemManager() XCTAssertEqual(sut.doneCount, 0, "Initially done count should be 0") }
To make this test pass, add the following property definition to ItemManager
:
let doneCount = 0
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
:
var sut: ItemManager!
Then, at the end of setUp()
, add this initialization of sut
:
sut = ItemManager()
Now, we can remove it from the tests:
func testToDoCount_Initially_ShouldBeZero() { XCTAssertEqual(sut.toDoCount, 0, "Initially toDo count should be 0") } func testDoneCount_Initially_ShouldBeZero() { XCTAssertEqual(sut.doneCount, 0, "Initially done count should be 0") }
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
:
func testToDoCount_AfterAddingOneItem_IsOne() { sut.addItem(ToDoItem(title: "Test title")) }
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(_:)
:
func addItem(item: ToDoItem) { }
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()
:
XCTAssertEqual(sut.toDoCount, 1, "toDoCount should be 1")
Run the tests. The tests fail because toDoCount
is a constant, and therefore, it never changes. Replace the highlighted lines in ItemManager
:
class ItemManager { var toDoCount = 0 let doneCount = 0 func addItem(item: ToDoItem) { ++toDoCount } }
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
:
func testItemAtIndex_ShouldReturnPreviouslyAddedItem() { let item = ToDoItem(title: "Item") sut.addItem(item) let returnedItem = sut.itemAtIndex(0) }
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
:
func itemAtIndex(index: Int) -> ToDoItem { return ToDoItem(title: "") }
It is the simplest implementation that makes the test code compilable again. Now, add the following assert to testItemAtIndex_ShouldReturnPreviouslyAddedItem()
:
XCTAssertEqual(item.title, returnedItem.title, "should be the same item")
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:
class ItemManager { var toDoCount = 0 let doneCount = 0 private var toDoItems = [ToDoItem]() func addItem(item: ToDoItem) { ++toDoCount toDoItems.append(item) } func itemAtIndex(index: Int) -> ToDoItem { return toDoItems[index] } }
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
:
func testCheckingItem_ChangesCountOfToDoAndOfDoneItems() { sut.addItem(ToDoItem(title: "First Item")) sut.checkItemAtIndex(0) }
This code does not compile because there is no checkItemAtIndex(_:)
method in ItemManager
. To make the test code compilable, add it to ItemManager
:
func checkItemAtIndex(index: Int) { }
When the user checks an item, toDoCount
should decrease and doneCount
should increase. Add the following asserts to testCheckingItem_ChangesCountOfToDoAndOfDoneItems()
:
XCTAssertEqual(sut.toDoCount, 0, "toDoCount should be 0") XCTAssertEqual(sut.doneCount, 1, "doneCount should be 1")
To make this test pass, we simply decrease and increase the values. A possible implementation could look like this:
class ItemManager { var toDoCount = 0 var doneCount = 0 private var toDoItems = [ToDoItem]() func addItem(item: ToDoItem) { ++toDoCount toDoItems.append(item) } func itemAtIndex(index: Int) -> ToDoItem { return toDoItems[index] } func checkItemAtIndex(index: Int) { --toDoCount ++doneCount } }
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:
func testCheckingItem_RemovesItFromTheToDoItemList() { let firstItem = ToDoItem(title: "First") let secondItem = ToDoItem(title: "Second") sut.addItem(firstItem) sut.addItem(secondItem) sut.checkItemAtIndex(0) XCTAssertEqual(sut.itemAtIndex(0).title, secondItem.title) }
This test fails. To make it pass, add the following line to checkItemAtIndex(_:)
:
_ = toDoItems.removeAtIndex(index)
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
:
func testDoneItemAtIndex_ShouldReturnPreviouslyCheckedItem() { let item = ToDoItem(title: "Item") sut.addItem(item) sut.checkItemAtIndex(0) let returnedItem = sut.doneItemAtIndex(0) }
Before we can continue writing the test, we need to add doneItemAtIndex(_:)
to ItemManager
:
func doneItemAtIndex(index: Int) -> ToDoItem { return ToDoItem(title: "") }
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()
:
XCTAssertEqual(item.title, returnedItem.title, "should be the same item")
This test fails because we return a dummy item from doneItemAtIndex(_:)
. To make it pass, replace the implementation of ItemManager
with the following code:
class ItemManager { var toDoCount = 0 var doneCount = 0 private var toDoItems = [ToDoItem]() private var doneItems = [ToDoItem]() func addItem(item: ToDoItem) { ++toDoCount toDoItems.append(item) } func checkItemAtIndex(index: Int) { let item = toDoItems.removeAtIndex(index) doneItems.append(item) --toDoCount ++doneCount } func itemAtIndex(index: Int) -> ToDoItem { return toDoItems[index] } func doneItemAtIndex(index: Int) -> ToDoItem { return doneItems[index] } }
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:
var toDoCount: Int { return toDoItems.count } var doneCount: Int { return doneItems.count }
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:
XCTAssertEqual(item.title, returnedItem.title, "should be the same item")
But we would like to write them like this:
XCTAssertEqual(item, returnedItem, "should be the same item")
If we try to do this and run the tests, we get this error:
error: cannot invoke 'XCTAssertEqual' with an argument list of type '(ToDoItem, ToDoItem, String)'
To figure out what this means, let's have a look at the definition of XCTAssertEqual
:
public func XCTAssertEqual<T : Equatable>(@autoclosure expression1: () -> T?, @autoclosure _ expression2: () -> T?, _ message: String = default, file: String = default, line: UInt = default)
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:
XCTAssertEqual(item.title, returnedItem.title, "should be the same item")
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:
func testEqualItems_ShouldBeEqual() { let firstItem = ToDoItem(title: "First") let secondItem = ToDoItem(title: "First") XCTAssertEqual(firstItem, secondItem) }
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:
struct ToDoItem : Equatable { // … }
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:
public protocol Equatable { @warn_unused_result public func ==(lhs: Self, rhs: Self) -> Bool }
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:
func ==(lhs: ToDoItem, rhs: ToDoItem) -> Bool { return true }
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:
func testWhenLocationDifferes_ShouldBeNotEqual() { let firstItem = ToDoItem(title: "First title", itemDescription: "First description", timeStamp: 0.0, location: Location(name: "Home")) let secondItem = ToDoItem(title: "First title", itemDescription: "First description", timeStamp: 0.0, location: Location(name: "Office")) XCTAssertNotEqual(firstItem, secondItem) }
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:
func ==(lhs: ToDoItem, rhs: ToDoItem) -> Bool { if lhs.location != rhs.location { return false } return true }
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:
func ==(lhs: ToDoItem, rhs: ToDoItem) -> Bool {
if lhs.location?.name != rhs.location?.name {
return false
}
return true
}
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
:
func testEqualLocations_ShouldBeEqual() { let firstLocation = Location(name: "Home") let secondLoacation = Location(name: "Home") XCTAssertEqual(firstLocation, secondLoacation) }
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:
struct Location : Equatable { // … }
Add the the dummy implementation of the equivalence operator in Location.swift
, but outside of the Location
struct:
func ==(lhs: Location, rhs: Location) -> Bool { return true }
Run the tests. All the tests pass again, and at this point, there is nothing to refactor. Add the following test:
func testWhenLatitudeDifferes_ShouldBeNotEqual() { let firstCoordinate = CLLocationCoordinate2D(latitude: 1.0, longitude: 0.0) let firstLocation = Location(name: "Home", coordinate: firstCoordinate) let secondCoordinate = CLLocationCoordinate2D(latitude: 0.0 longitude: 0.0 let secondLocation = Location(name: "Home", coordinate: secondCoordinate) XCTAssertNotEqual(firstLocation, secondLocation) }
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:
func ==(lhs: Location, rhs: Location) -> Bool { if lhs.coordinate?.latitude != rhs.coordinate?.latitude { return false } return true }
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:
func testWhenLongitudeDifferes_ShouldBeNotEqual() { let firstCoordinate = CLLocationCoordinate2D(latitude: 0.0, longitude: 1.0) let firstLocation = Location(name: "Home", coordinate: firstCoordinate) let secondCoordinate = CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0) let secondLocation = Location(name: "Home", coordinate: secondCoordinate) XCTAssertNotEqual(firstLocation, secondLocation) }
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:
func ==(lhs: Location, rhs: Location) -> Bool { if lhs.coordinate?.latitude != rhs.coordinate?.latitude { return false } if lhs.coordinate?.longitude != rhs.coordinate?.longitude { return false } return true }
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:
func performNotEqualTestWithLocationProperties(firstName: String, secondName: String, firstLongLat: (Double, Double)?, secondLongLat: (Double, Double)?) { let firstCoord: CLLocationCoordinate2D? if let firstLongLat = firstLongLat { firstCoord = CLLocationCoordinate2D( latitude: firstLongLat.0, longitude: firstLongLat.1) } else { firstCoord = nil } let firstLocation = Location(name: firstName, coordinate: firstCoord) let secondCoord: CLLocationCoordinate2D? if let secondLongLat = secondLongLat { secondCoord = CLLocationCoordinate2D( latitude: secondLongLat.0, longitude: secondLongLat.1) } else { secondCoord = nil } let secondLocation = Location(name: secondName, coordinate: secondCoord) XCTAssertNotEqual(firstLocation, secondLocation) }
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:
func testWhenLatitudeDifferes_ShouldBeNotEqual() { performNotEqualTestWithLocationProperties ("Home", secondName: "Home", firstLongLat: (1.0, 0.0), secondLongLat: (0.0, 0.0)) }
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:
if lhs.coordinate?.latitude != rhs.coordinate?.latitude { return false }
Run the test. The test does, indeed, fail but the failure shows in the line where XCTAssertNotEqual
is located:
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(…)
:
line: UInt = __LINE__
As the method now has a default value for the last argument, we can remove it from the call:
performNotEqualTestWithLocationProperties("Home", secondName: "Home", firstCoordinate: (1.0, 0.0), secondCoordinate: (0.0, 0.0))
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:
if lhs.coordinate?.latitude != rhs.coordinate?.latitude { return false }
Run the tests to make sure that all of them pass again. Now, replace testWhenLongitudeDifferes_ShouldBeNotEqual()
with the following code:
func testWhenLongitudeDifferes_ShouldBeNotEqual() { performNotEqualTestWithLocationProperties("Home", secondName: "Home", firstCoordinate: (0.0, 1.0), secondCoordinate: (0.0, 0.0)) }
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:
func testWhenOneHasCoordinateAndTheOtherDoesnt_ShouldBeNotEqual() { performNotEqualTestWithLocationProperties("Home", secondName: "Home", firstLongLat: (0.0, 0.0), secondLongLat: nil) }
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:
func testWhenNameDifferes_ShouldBeNotEqual() { performNotEqualTestWithLocationProperties("Home", secondName: "Office", firstLongLat: nil, secondLongLat: nil) }
This test fails. Add the following if
condition right before the return true
line in the implementation of the equivalence operator:
if lhs.name != rhs.name { return false }
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):
func ==(lhs: ToDoItem, rhs: ToDoItem) -> Bool { if lhs.location != rhs.location { return false } return true }
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:
func testWhenOneLocationIsNilAndTheOtherIsnt_ShouldBeNotEqual() { let firstItem = ToDoItem(title: "First title", itemDescription: "First description", timestamp: 0.0, location: nil) let secondItem = ToDoItem(title: "First title", itemDescription: "First description", timestamp: 0.0, location: Location(name: "Office")) XCTAssertNotEqual(firstItem, secondItem) }
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()
:
firstItem = ToDoItem(title: "First title", itemDescription: "First description", timestamp: 0.0, location: Location(name: "Home")) secondItem = ToDoItem(title: "First title", itemDescription: "First description", timestamp: 0.0, location: nil) XCTAssertNotEqual(firstItem, secondItem)
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:
func testWhenTimestampDifferes_ShouldBeNotEqual() { let firstItem = ToDoItem(title: "First title", itemDescription: "First description", timestamp: 1.0) let secondItem = ToDoItem(title: "First title", itemDescription: "First description", timestamp: 0.0) XCTAssertNotEqual(firstItem, secondItem) }
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:
if lhs.timestamp != rhs.timestamp { return false }
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:
func testWhenDescriptionDifferes_ShouldBeNotEqual() { let firstItem = ToDoItem(title: "First title", itemDescription: "First description") let secondItem = ToDoItem(title: "First title", itemDescription: "Second description") XCTAssertNotEqual(firstItem, secondItem) }
Adding the following if
condition to the equivalence operator right before the return true
statement, makes the test pass:
if lhs.itemDescription != rhs.itemDescription { return false }
The last thing we have to check is whether two to-do items differ if their titles differ. Add this test:
func testWhenTitleDifferes_ShouldBeNotEqual() { let firstItem = ToDoItem(title: "First title") let secondItem = ToDoItem(title: "Second title") XCTAssertNotEqual(firstItem, secondItem) }
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:
if lhs.title != rhs.title { return false }
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
:
func testRemoveAllItems_ShouldResultInCountsBeZero() { sut.addItem(ToDoItem(title: "First")) sut.addItem(ToDoItem(title: "Second")) sut.checkItemAtIndex(0) XCTAssertEqual(sut.toDoCount, 1, "toDoCount should be 1") XCTAssertEqual(sut.doneCount, 1, "doneCount should be 1") sut.removeAllItems() }
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:
func removeAllItems() { }
Now, add the following assertions to testRemoveAllItems_ShouldResultInCountsBeZero()
to check whether the items have been removed:
XCTAssertEqual(sut.toDoCount, 0, "toDoCount should be 0") XCTAssertEqual(sut.doneCount, 0, "doneCount should be 0")
To make this test pass, we need to remove all the items from the underlying arrays. Add the following implementation in removeAllItems()
:
toDoItems.removeAll() doneItems.removeAll()
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
:
func testAddingTheSameItem_DoesNotIncreaseCount() { sut.addItem(ToDoItem(title: "First")) sut.addItem(ToDoItem(title: "First")) XCTAssertEqual(sut.toDoCount, 1) }
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:
func addItem(item: ToDoItem) { if !toDoItems.contains(item) { toDoItems.append(item) } }
Run the tests. All the tests pass, and we are finally finished with the implementation of our model.