Swift & Nimble Testing

I’m a big fan of unit testing and when I discovered the BDD framework Nimble with Swift support a couple of months ago I was delighted. One of its advantages is that instead of using the XCTAssertEqual macros you can write:

expect(answer).to.equal(42)

which, thanks to Swift’s support for operator overloading, can be made even more expressive:

expect(answer) == 42

However, there was one area where I found the syntax a bit verbose – when comparing floating point numbers:

expect(answer).to(beCloseTo(42.0))

What this does is simply make the comparison fuzzy by allowing a difference of 0.0001 – the default delta as defined by the framework.

It would be great if we could write it this way:

expect(answer) ≈ 42.0    // type Option-x for ≈ (U.S. keyboard)

And actually we can, thanks to Swift’s support for custom operators. Now understandably people worry about misuse of custom operators and I fully agree that you need to be very careful when and where you use them. But I feel that test code is a good place where a readability ‘optimisation’ like this one can be applied.

All unit tests do is compare expectations to actuals and anything we can do to make this concise and readable for the actual values stand out over the boilerplate is a win. Custom operators are a good tool for this, especially if they mirror universal symbols like the mathematical sign of inequality.

Justifications aside, how does this work?

Nimble allows you to define so-called custom matchers that extend the set of validations:

public func equal<T: Equatable>(expectedValue: T?) -> MatcherFunc<T?> {
  return MatcherFunc { actualExpression, failureMessage in
    failureMessage.postfixMessage = "equal <\(expectedValue)>"
    return actualExpression.evaluate() == expectedValue
  }
}

The existing package already provides a beCloseTo matcher for decimal number comparisons and it is then straightforward to define an operator for it:

infix operator ≈ {}
public func ≈(lhs: Expectation<Double>, rhs: Double) {
    lhs.to(beCloseTo(rhs))
}

But what’s missing here is the case where you specify a delta different from the default:

expect(answer).to(beCloseTo(42.0, within: 1.0)

In other words if we want to specify the delta (and that’s probably quite common) we’re back to the more verbose version. Ideally we’d like to write this as:

expect(answer) == 42.0 ± 1.0    // type Option-Shift-= for ± (U.S. keyboard)

Turns out we can, and the way this works is as follows. First we create a binary operator ± that converts the value to its left and the delta to its right into a tuple (expected: Double, delta: Double):

infix operator ± { precedence 170 }
public func ±(lhs: Double, rhs: Double) -> (expected: Double, delta: Double) {
    return (expected: lhs, delta: rhs)
}

Then we add an overloaded method which takes an Expectation<Double> and the tuple (expected: Double, delta: Double) as parameters:

public func ≈(lhs: Expectation<Double>, rhs: (expected: Double, delta: Double)) {
    lhs.to(beCloseTo(rhs.expected, within: rhs.delta))
}

These changes have been kindly accepted and integrated by the Nimble team into the framework as of Jan 5 (commit e7bafdb)

Part of this change was also an extension for comparisons of arrays of numbers:

expect([0.0, 1.1, 2.2]) ≈ [0.0001, 1.1001, 2.2001]
expect([0.0, 1.1, 2.2]).to(beCloseTo([0.1, 1.2, 2.3], within: 0.1))

See the Nimble documentation for further examples.