Navigating Equatable in Swift for Effective Unit Testing?
“Equatable” means that an object can be compared for equality with another object of the same type.
Equatable in Swift
In Swift, you make a type equatable by conforming it to the Equatable
protocol. This protocol requires the implementation of the ==
operator.
Here’s an example of a simple struct conforming to Equatable
:
struct Person: Equatable {
var name: String
var age: Int
// Automatically synthesizes the `==` operator
// if all properties are `Equatable`
}
// Alternatively, you can manually implement the `==` operator:
extension Person {
static func == (lhs: Person, rhs: Person) -> Bool {
return lhs.name == rhs.name && lhs.age == rhs.age
}
}
What is Equatable
In Swift, certain types are equatable by default, meaning that they automatically conform to the Equatable
protocol without requiring any additional implementation. These types include:
Standard Library Types:
- Numeric Types: Integer and floating-point types (
Int
,UInt
,Double
,Float
, etc.). - Bool: The Boolean type (
Bool
) is equatable by default. - Character: The character type (
Character
) is equatable. - String: The string type (
String
) is equatable. - Optional: Optional types (
Optional<T>
, or its syntactic sugarT?
) are equatable if the wrapped typeT
is equatable. - Array, Dictionary, Set: Collection types (
Array
,Dictionary
,Set
) are equatable if their elements are equatable. - Range, ClosedRange: Range types (
Range
,ClosedRange
) are equatable if their bounds are equitable. - Tuple Types: Tuple types are equatable if their element types are equitable.
// Standard library types
let x: Int = 42
let y: Double = 3.14
let a: String = "Hello"
let b: [Int] = [1, 2, 3]
// Tuple types
let tuple1 = (1, "abc")
let tuple2 = (true, 42)
// Equatable check
print(x == 42) // true
print(a == "Hello") // true
print(b == [1, 2, 3]) // true
print(tuple1 == (1, "abc")) // true
print(tuple2 == (true, 42)) // true
In the above example, all the types used (Int
, Double
, String
, [Int]
, and tuples) are equatable by default, so they can be compared using the equality operator (==
) without any explicit conformance to the Equatable
protocol.
Custom types that explicitly conform to Equatable
As shown in the Person
struct example above, custom types can be made equatable by conforming to the Equatable
protocol and implementing the ==
operator.
Types That Do Not Conform to Equatable by Default
Custom Classes without Equatable
Conformance:
Classes do not automatically conform to Equatable
. You must implement the ==
operator manually:
class Animal: Equatable {
static func == (lhs: Animal, rhs: Animal) -> Bool {
return lhs.name == rhs.name
}
var name: String
init(name: String) {
self.name = name
}
}
Equatable for Enums
in Swift, enums are automatically equatable by default if they do not have associated values. If an enum has associated values, it can still be equatable, but the compiler will only synthesize the ==
operator if all the associated values are themselves equatable.
Basic Enums
For simple enums without associated values, Swift automatically provides Equatable
conformance:
enum Direction: String {
case east
case west
}
let direction1 = Direction.east
let direction2 = Direction.east
let direction3 = Direction.west
print(direction1 == direction2) // true
print(direction1 == direction3) // false
Enums with Associated Values
For enums with associated values, Swift will automatically synthesize the ==
operator if all associated values are equitable:
enum Result {
case success(message: String)
case failure(error: String)
}
// Since String is equatable, the Result enum gets synthesized `==` operator.
let result1 = Result.success(message: "Data loaded")
let result2 = Result.success(message: "Data loaded")
let result3 = Result.failure(error: "Network error")
print(result1 == result2) // true
print(result1 == result3) // false
If the associated values are not equatable, you need to manually implement the
==
operator:
struct CustomError {
let code: Int
let message: String
}
enum Result {
case success(message: String)
case failure(error: CustomError)
}
// implement the Equatable
extension Result: Equatable {
static func == (lhs: Result, rhs: Result) -> Bool {
switch (lhs, rhs) {
case (.success(let lhsMessage), .success(let rhsMessage)):
return lhsMessage == rhsMessage
case (.failure(let lhsError), .failure(let rhsError)):
return lhsError.code == rhsError.code && lhsError.message == rhsError.message
default:
return false
}
}
}
let customError1 = CustomError(code: 404, message: "Not found")
let customError2 = CustomError(code: 404, message: "Not found")
let customError3 = CustomError(code: 500, message: "Server error")
let result1 = Result.failure(error: customError1)
let result2 = Result.failure(error: customError2)
let result3 = Result.failure(error: customError3)
print(result1 == result2) // true
print(result1 == result3) // false
Equatable for Structs
Structs automatically conform to Equatable
if all their properties are Equatable
. Your Person
struct demonstrates this:
Important the synthesized ==
operator for structs is only automatically generated if the struct explicitly conforms to the Equatable
protocol. This means that simply having Equatable
properties is not enough; the struct itself must declare conformance to Equatable
.
// no compilation error in this case
struct Person: Equatable {
var name: String
var age: Int
}
Unit Testing with Equatable
In unit testing, you often need to compare objects to ensure they have the expected values. Conforming to Equatable
simplifies this process, as it allows the use of XCTAssertEqual
to compare objects directly.
In this example, the XCTAssertEqual
leverage the Equatable
conformance to compare the Person
instances.
Conclusion
Understanding what is and isn’t equatable in Swift is essential for effective unit testing. Non-equatable types require more effort to compare, but by conforming to the Equatable
protocol and ensuring all properties are also equatable, you can simplify comparisons and make your unit tests more robust.