Holy Swift

Holy Swift

Curious Ways To Solve Tests Equality Problems in Swift

Curious Ways To Solve Tests Equality Problems in Swift

It looks the same, but is it?

Subscribe to my newsletter and never miss my upcoming articles

Hallo keizerinnen en keizers, Leo hier.

Today we will explore a somewhat common problem when testing code in Swift/iOS development. I can't stress enough the fact we all should be doing tests, this is a real game changer for any developer.

A long time ago I've watched a lecture about code standards and the speaker said something that echoes in me until today:

"What reasoning someone do to write a line of code and think: "Well I don't have to test this line." When you put this into words, sounds weird isn't it?"

I really hope that all my audience make tests for their code, this leads inevitably to comparisons. Either you use XCTAssertFalse, XCTAssertNil or any other assertion method eventually you will use some kind of equality. This post is about test equality and how you can compare two things in tests. So be prepared to some non trivial syntax and various ways to solve the same problem.

Let's test! But first...

The painting

This is 1664 masterpiece called Crimean Falconer of King John Casimir from the genius Daniel Schultz. Born somewhere around 1615 in Gdańsk (Danzig) in Pomeranian Voivodeship, Polish–Lithuanian Commonwealth, Schultz learned the art of painting from his uncle, Daniel Schultz the Elder, another important painter, working in his workshop for about five years. After his uncle's death he travelled to France and the Netherlands to continue his studies for about three years. The inspirations of Rembrandt and Philippe de Champaigne are visible in his works.

I choose this painting because if you pay attention to the most right kid and the king in the middle, they have a very very similar face, but they are not equal... are they? As we will talk about equality, old painting with family are amazing place to acknowledge that not everything that looks the same, are the same.

The problem

Imagine that you have a complex type that you want to check it equality with another object in the test suite.

Imagine you have this code within your codebase:

protocol SimplePrint {
    func printAll()
}

struct SimpleManager: SimplePrint {
    func printAll() {
        print("All")
    }
}

struct ComplexType {
    let name: String
    let action: SimplePrint
}


let complex1 = ComplexType(name: "First", action: SimpleManager())
let complex2 = ComplexType(name: "Second", action: SimpleManager())

What happens if we have to check in the test suit if complex1 is equal complex2?

Screenshot 2021-10-07 at 08.48.11.png

Well the answer is you can't do that because the compiler can't infer how to compare those two objects, so we need to help him. Let's start with the most straightforward and Xcode recommendation strategy: equatable protocol.

Solution 1 - Equatable

First you should import XCTest and do a little setup to be able to test. Open Xcode Playground if you want to code along, and I recommend that. And let's dive into the problem:

class MyTestCase: XCTestCase {

    let complex1 = ComplexType(name: "First", action: SimpleManager())
    let complex2 = ComplexType(name: "Second", action: SimpleManager())

    func testMyCaseTestsNow() {
        XCTAssertEqual(complex1, complex1)
    }
}

MyTestCase.defaultTestSuite.run()

You get Global function 'XCTAssertEqual(::_:file:line:)' requires that 'ComplexType' conform to 'Equatable'.

So Swift recommend that you use equatable. Let's conform our ComplexType to Equatable.

struct ComplexType: Equatable {

    let name: String
    let action: SimplePrint

    static func == (lhs: ComplexType, rhs: ComplexType) -> Bool {
        return lhs.name == rhs.name
    }
}

Now you can use XCTAssertEqual on the type. But this could be improved right? Ideally we should only assert equality on object that we can uniquely identify or all the properties are not relevant to equality ( like money, a 5 dollar bill is equal other 5 dollar bill even being different bills). Today is our lucky day! Swift has it's own protocol that enforces this, let's use it:

struct ComplexType: Equatable, Identifiable {
    var id: String
    let name: String
    let action: SimplePrint

    static func == (lhs: ComplexType, rhs: ComplexType) -> Bool {
        return lhs.id == rhs.id
    }
}

Now when we compare two ComplexType structs we are sure that they are the same. Calling the tests again:

    func testMyCaseTestsNow() {
        let complex1 = ComplexType(id: UUID().uuidString, name: "First", action: SimpleManager())
        let complex2 = ComplexType(id: UUID().uuidString, name: "Second", action: SimpleManager())

        XCTAssertEqual(complex1, complex1)
    }

Solution 2 - Turn All Properties Into Equatable

Now things begin to be interesting. Now our ComplexType has three properties: Two Strings ( they are already equatable) and a SimplePrint type.

struct ComplexType: Equatable, Identifiable {
    var id: String
    let name: String
    let action: SimplePrint

    static func == (lhs: ComplexType, rhs: ComplexType) -> Bool {
        return lhs.id == rhs.id
    }
}

But how can we use equatable inference for ComplexType? We need that SimplePrint protocol be equatable too. Like this code below right....? :

let action: SimplePrint & Equatable

Not exactly, because the Equatable protocol has Self requirements. And the code below show how Generics come to safe the day again:

struct ComplexType<ActionType: SimplePrint & Equatable>: Identifiable {
    var id: String
    let name: String
    let action: ActionType
}

But now we have another problem. SimpleManager is not Equatable. This will force us two choose.... If we can turn SimpleManager a Equatable struct is solved, but if not we can also use a conditional protocol conformance, like the example below:

struct ComplexType<ActionType: SimplePrint>: Identifiable {
    var id: String
    let name: String
    let action: ActionType
}

extension ComplexType: Equatable where ActionType: Equatable {}

This way our ComplexType can be equatable or not depending on what conform to ActionType. Really amazing, isn't it?

Now we have to change the tests and create some manager that conform to Equatable and SimplePrint:

struct SimpleComparableManager: SimplePrint, Equatable, Identifiable {
    var id: String

    func printAll() {
        print("All")
    }
}

class MyTestCase: XCTestCase {

    func testMyCaseTestsNow() {
        let complex1 = ComplexType(id: UUID().uuidString, name: "First", action: SimpleComparableManager(id: "Manager 1"))
        //let complex2 = ComplexType(id: UUID().uuidString, name: "Second", action: SimpleManager())

        XCTAssertEqual(complex1, complex1)
    }
}

Solution 3 - Use Enums

So if you are sure that you have limited and well defined implementations of the SimplePrint protocol, you can substitute that type by a enum in the ComplexType, check the enum below:

enum PrintManager: Equatable {
    case simple(SimpleManager)
    case comparable(SimpleComparableManager)

    static func == (lhs: PrintManager, rhs: PrintManager) -> Bool {
        switch (lhs, rhs) {
        case (.simple(_), .simple(_)):
            return true
        case let (.comparable(a), .comparable(b)):
            return a == b
        case (.simple(_), _):
            return false
        case (.comparable(_),_):
            return false
        }
    }
}

This way you can still here we force the enum itself to implement the Equatable protocol to give us freedom for our types to not implement at all! Now fixing the ComplexType and the tests:

struct ComplexType: Identifiable, Equatable {
    var id: String
    let name: String
    let action: PrintManager
}

class MyTestCase: XCTestCase {

    func testMyCaseTestsNow() {
        let complex1 = ComplexType(id: UUID().uuidString, name: "First", action: .comparable(SimpleComparableManager(id: "123123")))
        //let complex2 = ComplexType(id: UUID().uuidString, name: "Second", action: SimpleManager())

        XCTAssertEqual(complex1, complex1)
    }
}

And... Voilà! You have your comparison and the flexibility to yours managers not being necessarily equatable. By the way, this is the only way to compare two things that are not Equatable, you have to wrap your not Equatable Type into a Equatable one... Or maybe there's others ways!

Solution 4 - Using XCTAssertIdentical and XCTAssertNotIdentical

For this you will need to transform your structs into classes. This way you leave behind the value type nature of you object and embrace the reference type nature. Enabling us to compare not only values, but also identities. Let's do the code changes:

class ComplexType: Identifiable {
    var id: String
    let name: String
    let action: SimplePrint

    init(id: String, name: String, action: SimplePrint) {
        self.id = id
        self.name = name
        self.action = action
    }
}

We have to add the initialiser because we don't receive a default one from classes. (Well, I know some classes you can create without init but this is topic for another post). Now we can compare their identities:

class MyTestCase: XCTestCase {

    func testMyCaseTestsNow() {
        let complex1 = ComplexType(id: UUID().uuidString, name: "First", action: SimpleComparableManager(id: "123123"))

        XCTAssertIdentical(complex1, complex1)
    }
}

And of course we have the nonIdentical too:

class MyTestCase: XCTestCase {

    func testMyCaseTestsNow() {
        let complex1 = ComplexType(id: UUID().uuidString, name: "First", action: SimpleComparableManager(id: "123123"))
        let complex2 = ComplexType(id: UUID().uuidString, name: "First", action: SimpleComparableManager(id: "123123"))

        XCTAssertNotIdentical(complex1, complex2)
    }
}

Important note: When comparing classes with XCTAssertIdentical or XCTAssertNotIdentical you are just testing with both of the variables points to the same object in the memory. You are not pointing to values here, keep this in your head!

Solution 5 - The Legendary Triple Equal Identity Operator

Another way to compare pointers in the memory ( classes ) is using the legendary triple equals identity operator ===:

class MyTestCase: XCTestCase {

    func testMyCaseTestsNow() {
        let complex1 = ComplexType(id: UUID().uuidString, name: "First", action: SimpleComparableManager(id: "123123"))

        XCTAssertTrue(complex1 === complex1)
    }
}

It's the same result as using XCTAssertIdentical.

Solution 6 - Use ObjectIdentifier

Same as above but using the struct ObjectIdentifier to unique identify a class instance or metatype.

class MyTestCase: XCTestCase {

    func testMyCaseTestsNow() {
        let complex1 = ComplexType(id: UUID().uuidString, name: "First", action: SimpleComparableManager(id: "123123"))

        XCTAssertEqual(ObjectIdentifier(complex1), ObjectIdentifier(complex1))
    }
}

And we are done!

Summary

Today we learn many ways to do object comparison in Swift. Using structs with generics with optional conformance protocol, or enums to wrap the non-equatable content or making it a class and comparing pointers in memory, the most important thing to keep in mind is what makes sense to your codebase. If using structs is fine, so let it be. Now we've explored a lot of options for you and I'm sure that are many other ones!

That's all my people, I hope you liked as I enjoyed write this article. If you want to support this blog you can Buy Me a Coffee or just leave a comment saying hello. You can also sponsor posts! Just reach me in LinkedIn or Twitter for details.

Thanks for the reading and... That's all folks.

credits: image

Interested in reading more such articles from Leonardo Maia Pugliese?

Support the author by donating an amount of your choice.

 
Share this