Hallo keizerinnen en keizers, Leo hier. The topic today is various ways to do equatable in Unit Tests using Swift.
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 does someone does 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 for some nontrivial syntax and various ways to solve the same problem.
Let’s test! But first…
Painting of The Day
This is a 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 vital painter, working in his workshop for about five years. After his uncle’s death, he traveled 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 faces, but they are not equal… are they? As we will talk about equality, an old painting with a family is an amazing example to acknowledge that not everything that looks the same, is the same.
The problem – Best ways to do Equatable in Unit Tests using Swift
You have a complex type that you want to check its 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
?
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 Xcode recommendation strategy: the equatable
protocol.
Solution 1 – Conforming to 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 recommends 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
function on the object. But this could be improved, right?
Using Identifiable
Ideally, we should only assert equality on objects 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 its 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) }
And this was the first solution!
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 to 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 shows how Generics come to save 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 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 conforms to ActionType
. Really amazing, isn’t it?
Now we have to change the tests and create some managers 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) } }
That was our second possible alternative to solve the unit test equality problem!
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 an 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 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 your 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 Equatable
one… Or maybe there are other 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 of your object and embrace the reference type. Enabling us to compare not only values but also memory addresses.
Let’s do the code changes:
class ComplexType: Identifiable { // now is a class 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 initializer because we don’t receive a default one from classes. (Well, I know some classes you can create without init but this is a topic for another article).
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 if both of the variables point to the same object in the memory. You are not pointing to values here, keep this in mind!
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 uniquely identify a class instance or meta type.
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 – Best way to do Equatable in Unit Tests in Swift
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 many options for you and I’m sure there are many other ones!
That’s all my people, I hope you liked reading this article as much as I enjoyed writing it. If you want to support this blog you can Buy Me a Coffee or leave a comment saying hello. You can also sponsor posts and I’m open to freelance writing!
You can reach me on LinkedIn or Twitter and send me an e-mail through the contact page.
Thanks for reading and… That’s all folks.
Credits: