Equatable

At the time of writing, conformance to the protocol Equatable had to be implemented by the developer. But at the same time, there is a discussion about whether the compiler will be able to generate the necessary code in the future. So, maybe by the time you read this section, it's enough to add Equatable in the definition of a class or struct to make it conform to Equatable. In this case, you can skip this section.

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 the Equatable conformance:

func text_EqualItems_AreEqual() { 
  let first = ToDoItem(title: "Foo") 
  let second = ToDoItem(title: "Foo") 
   

  XCTAssertEqual(first, second) 
}

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 3.0:

public protocol Equatable { 
    public static 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 test_Items_WhenLocationDiffers_AreNotEqual() { 
  

let first = ToDoItem(title: "",
location: Location(name: "Foo")) let second = ToDoItem(title: "",
location: Location(name: "Bar")) XCTAssertNotEqual(first, second) }

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 test_EqualLocations_AreEqual() { 
  let first = Location(name: "Foo") 
  let second = Location(name: "Foo") 
   
  XCTAssertEqual(first, second) 
} 

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 dummy implementation of the equivalence operator in Location.swift, but outside of the Location struct:

public static 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 test_Locations_WhenLatitudeDiffers_AreNotEqual() { 
  let firstCoordinate = 
CLLocationCoordinate2D(latitude: 1.0,
longitude: 0.0) let first = Location(name: "Foo",
coordinate: firstCoordinate) let secondCoordinate =
CLLocationCoordinate2D(latitude: 0.0,
longitude: 0.0) let second = Location(name: "Foo",
coordinate: secondCoordinate) XCTAssertNotEqual(first, second) }

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:

public static 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 returns 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 test_Locations_WhenLongitudeDiffers_AreNotEqual() { 
  let firstCoordinate = 
CLLocationCoordinate2D(latitude: 0.0, longitude: 1.0) let first = Location(name: "Foo",
coordinate: firstCoordinate) let secondCoordinate =
CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0) let second = Location(name: "Foo",
coordinate: secondCoordinate) XCTAssertNotEqual(first, second) }

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

public static 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 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 assertLocationNotEqualWith(firstName: String,
firstLongLat: (Double, Double)?,
secondName: String,
secondLongLat: (Double, Double)?) {

var firstCoord: CLLocationCoordinate2D? = nil
if let firstLongLat = firstLongLat {
firstCoord =
CLLocationCoordinate2D(latitude: firstLongLat.0,
longitude: firstLongLat.1)
}
let firstLocation =
Location(name: firstName,
coordinate: firstCoord)


var secondCoord: CLLocationCoordinate2D? = nil
if let secondLongLat = secondLongLat {
secondCoord =
CLLocationCoordinate2D(latitude: secondLongLat.0,
longitude: secondLongLat.1)
}
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 test_Locations_WhenLatitudeDiffers_AreNotEqual() with this:

func test_Locations_WhenLatitudeDiffers_AreNotEqual() {


assertLocationNotEqualWith(firstName: "Foo",
firstLongLat: (1.0, 0.0),
secondName: "Foo",
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 test 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 assertLocationNotEqualWith(...) and use it in the assertion:

func assertLocationNotEqualWith(firstName: String,
firstLongLat: (Double, Double)?,
secondName: String,
secondLongLat: (Double, Double)?,
line: UInt) {


// ...


XCTAssertNotEqual(firstLocation,
secondLocation,
line: line)
}

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

assertLocationNotEqualWith(firstName: "Foo",
firstLongLat: (1.0, 0.0),
secondName: "Foo",
secondLongLat: (0.0, 0.0), line: 64)

The number 64 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 on the specified line.

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 64 (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.

We can do better using default values for method arguments. Add a default value to the last argument of assertLocationNotEqualWith(...):

line: UInt = #line 

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

assertLocationNotEqualWith(firstName: "Foo",
firstLongLat: (1.0, 0.0),
secondName: "Foo",
secondLongLat: (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 test_Locations_WhenLongitudeDiffers_AreNotEqual() with the following code:

func test_Locations_WhenLongitudeDiffers_AreNotEqual() { 
  

assertLocationNotEqualWith(firstName: "Foo",
firstLongLat: (0.0, 1.0),
secondName: "Foo",
secondLongLat: (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 test_Locations_WhenOnlyOneHasCoordinate_AreNotEqual() {


assertLocationNotEqualWith(firstName: "Foo",
firstLongLat: (0.0, 0.0),
secondName: "Foo",
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 test_Locations_WhenNamesDiffer_AreNotEqual() {


assertLocationNotEqualWith(firstName: "Foo",
firstLongLat: nil,
secondName: "Bar",
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):

public static 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 test_Items_WhenOneLocationIsNil_AreNotEqual() {


let first = ToDoItem(title: "",
location: Location(name: "Foo"))
let second = ToDoItem(title: "",
location: nil)


XCTAssertNotEqual(first, second)
}

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 test_Items_WhenOneLocationIsNil_AreNotEqual():

first = ToDoItem(title: "", 
location: nil) second = ToDoItem(title: "",
location: Location(name: "Foo"))

XCTAssertNotEqual(first, second)

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 test_Items_WhenTimestampsDiffer_AreNotEqual() {

let first = ToDoItem(title: "Foo",
timestamp: 1.0) let second = ToDoItem(title: "Foo",
timestamp: 0.0) XCTAssertNotEqual(first, second) }

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 test_Items_WhenDescriptionsDiffer_AreNotEqual() { 

let first = ToDoItem(title: "Foo",
itemDescription: "Bar") let second = ToDoItem(title: "Foo",
itemDescription: "Baz") XCTAssertNotEqual(first, second) }

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 test_Items_WhenTitlesDiffer_AreNotEqual() { 
  let first = ToDoItem(title: "Foo") 
  let second = ToDoItem(title: "Bar") 
 
  
  XCTAssertNotEqual(first, second) 
} 

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.

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

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