Let’s Stop Overusing Swift Equatables in Unit Tests 

 January 31, 2017

by Jon Reid


Enumerations with associated values are my favorite feature of Swift. But how can we write unit tests against them? “Make them Equatable and use XCTAssertEqual” is common advice.

I’m here to argue otherwise. Let’s use this as a jumping-off point to discuss Swift Equatables in unit tests.

Code snippet sample

Improve your test writing “Flow.”

Sign up to get my test-oriented code snippets.

Problems with the “Equatable All the Things!” Approach

Equatables used to be much more painful when they weren’t synthesized by Swift. When we had to code them by hand, it was common for Swift developers to write these Equatable implementations and put them into production code, typically without tests.

Now that we have synthesized Equatables, we no longer need to unit test them. We can trust the Swift compiler. Believe it or not, I love not having to write tests!

Still, I would hesitate to make a type Equatable just because I want to assert something about it in a unit test.

Equatables in Production May Violate YAGNI

What’s wrong with putting Equatable implementations in production code? Nothing, as long as we need them for production code. Will the type be used in a sequence or collection? Then consider making it Equatable, depending on the algorithms you want to have available.

But not all types need proper value semantics. Most of the time, I’m just sending a bag of data from one point to another. Arguing “it should be Equatable, because it should have value semantics, because it’s a value object” strikes me as a violation of YAGNI: “You Aren’t Gonna Need It.” It’s coding for a future that may never come. It’s waste.

How to Use Enumerations in Tests

So what do we do instead? Let’s look specifically at enumerations with associated values.

I’ve shared a screencast of how to start JSON parsing in Objective-C. But a naive translation of that code into Swift is unsatisfying. If we send a response model containing a status code, then the response handler will need to examine this status first. It needs to call some logic to determine whether the response represents a success or a failure.

In Swift, a better way is to encode this into a Result enumeration. We now get Result types for free, but we used to define it ourselves,  something like this:

enum Result<T> {
    case success(T)
    case failure(String)

This forces us to consider success and failure up front. It also simplifies the response model, essentially moving the status code into the enumeration.

Let’s first write a test for a successful response. If the JSON contains a code of 200, we want the parse method of the System Under Test to return success.

private func jsonData(_ json: String) -> Data {
    return Data(json.utf8)
func test_parse_withCode200_shouldSucceed() {
    let json = "{\"code\":200}"
    let response = sut.parse(jsonData(json))
    guard case .success(_) = response else {
        XCTFail("Expected .success, but was \(response)")

Instead of an Equatable, we’re using a guard case statement. If we can’t convert the value to a .success, we fail. This code is resilient, and won’t have to be changed even if we add new enumeration values.

For other tests, we probably want to examine the associated data. We can extract it just by adding a let to the guard case.

For a deeper exploration of this topic, see How to Unit Test Enumerations in Swift.

Equality Is Overrated for Tests

Notice what the test does with the success value’s associated response model: nothing. This particular test doesn’t care. If we had written this using XCTAssertEqual, then we’d have to create an “expected value” of our response model. Every time a change was made to the response model, we’d have to update all tests that used it in an XCTAssertEqual assertion.

The problem with equality is that it’s so exact. Sometimes you want it, of course. But for many unit tests, it’s overkill. It can over-specify something that isn’t relevant to the outcome of the test. This results in fragile tests.

Testing for equality also inhibits test-driven development of aggregate types. TDD is a combination of Test-First with Incremental Design. I want to grow the JSON parsing code iteratively, using subsets of JSON that affect different slices of the response. This will happen gradually, over many tests. I want to avoid big-bang comparisons.

Testing for equality inhibits test-driven development of aggregate types.

Click to Tweet

In Summary

Let’s see if I can boil all this down to “Jon’s Testing Rules of Thumb.” (I should add, “for now,” because this is a process of discovery.)

  • Don’t define something as Equatable in production code unless it’s needed by production code. You can always create an Equatable extension in test code if you want.
  • Testing for equality is usually fine for single-value results. But for aggregate values, XCTAssertEqual assertions can result in fragile tests.
  • For enumerations with associated values, use a guard case statement in your tests. Add a let to extract the associated values you want to examine. Read more about the ins and outs of testing enumerations in How to Test Enumerations in Swift.

Questions, comments, concerns? Please leave a comment below!

[This post is part of the series TDD Sample App: The Complete Collection …So Far]

Jon Reid

About the author

Programming was fun when I was a kid. But working in Silicon Valley, I saw poor code lead to fear, with real human costs. Looking for ways to make my life better, I learned about Extreme Programming, including unit testing, test-driven development (TDD), and refactoring. Programming became fun again! I've now been doing TDD in Apple environments for 20 years. I'm committed to software crafting as a discipline, hoping we can all reach greater effectiveness and joy. Now a coach with Industrial Logic!

  • What about avoiding logic in tests?

    The guard statement is a conditional statement. Is this just a trade off we have to make for this scenario?

  • {"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}