Let’s Stop Overusing Swift Equatables in Unit Tests

Shares

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. In fact, let’s use this as a jumping-off point to discuss Swift Equatables in unit tests.

Apples and oranges: Sometimes you shouldn't care about Swift equatable testing

Problems with the “Equatable All the Things!” approach

When faced with the need to write unit tests, most Swift developers I’ve seen have an immediate response: make the type Equatable. This knee-jerk response creates a series of cascading problems:

  1. We have to write all these Equatable implementations. Lots of them.
  2. There’s a tendency to put these Equatable implementations into production code.
  3. Folks use them to write XCTAssertEqual statements right away, without first unit testing their Equatable implementations.

One of my colleagues has this Twitter bio: “Writing Equatable implementations at American Express”.

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 possibility which may never come. It’s waste.

Swift Equatables need testing, too!

Having written their Equatables, people have a tendency to use them right away. I want to say, “Whoa, slow down there.”

First, that’s not Test Driven Development. In TDD, a test comes first. Unless you’re laying out views, resist the urge to start by writing production code.

Second, whether the Equatable is placed in production code or in test code, it’s code. So it needs unit tests. If you put it in your test target, good for you. But in that case, you’re writing a test helper. “Who watches the watchmen?” That is, who tests the tests? One way is to extract helpers from existing tests. Another is to write tests for the helper, before using it.

But we shouldn’t write test helper code, and tests that use those helpers, at the same time. That’s asking for trouble.

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 an enumeration. Something like this:

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

where T is the type of the response model. This not only forces response handler 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 json.data(using: .utf8)!
    }

    func testParse_WithCode200_ShouldReturnSuccess() {
        let json = "{\"code\":200}"

        let response = sut.parse(jsonData(json))

        switch response {
        case .success(_):
            break
        default:
            XCTFail("Expected success, got \(response)")
        }
    }

Instead of an Equatable, we’re using a switch statement. If it’s a success, this test does nothing. For any other value, we fail. This code is resilient, and won’t have to be changed even if we add new enumeration values.

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 really 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.

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 put Equatable implementations into production code, unless they’re needed by production code.
  • Write unit tests that drive correct Equatable implementations. Do this before using them in XCTAssertEqual assertions.
  • 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, go ahead and use a switch statement in your tests. Deal with the expected value, and lump all other values into a default case failure.

I wouldn’t want to see similar switch statements repeated across tests. Ultimately, there’s probably a test helper waiting to be extracted. But that’s a topic for another time…

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

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

About the Author Jon Reid

Jon is a coach and consultant on iOS Clean Code (Test Driven Development, unit testing, refactoring, design). He's been practicing TDD since 2001. You can learn more about his background, or see what services he can bring to your organization.

follow me on:

Leave a Comment:

2 comments
Add Your Reply