Testing Of Enumerations Made Easy
31 Oct 2015

Have you ever wondered how to test enumerations with associated values easily in Swift? The point is that you can do all sorts of pattern matching with enums, but you cannot express a simple expectation in a single line. Well, with a little help from a function, you actually can.

Let't talk about Nimble

In case you haven't heard of Nimble yet, you should definitely have a look at it. It enables you to write short and expressive expectations in your unit tests. Here is an example:This example has been adopted a while ago by Airspeed Velocity. For the uninitiated: watch this, or for the full fun this.

let answer = kingArthur.askForAirspeedVelocityOf("unladen swallow")
expect(answer).to(contain("African"))
expect(answer).to(contain("European"))

Once you get familiar with this style of writing tests you will love it. The problem is: this simplicity is lost once enumerations with associated values are involved.

Let's suppose the result of our example function askForAirspeedVelocityOf is a proper velocity value, not a counter question. Furthermore, we can distinguish multiple types of swallows. Therefore a proper return type could be defined as follows:

enum SwallowVelocity {
    case African(Int)
    case European(Int)
    case Other(Int)
}

Let's say, the only correct answer is a velocity of 11 meters per second for the European swallow. How do you write a test for this? The following is illegal, because enumerations with associated values are not Equatable by default:You can try to add Equatable, but that's also far away from a short solution, see this interesting blog post by Fabián Cañas.

expect(answer).to(equal(SwallowVelocity.European(11)))

Instead you have to use pattern matchingBenedikt Terhechte has written an excellent article about all aspects of pattern matching., for example in combination with if:

if case let .European(velocity) = answer {
    expect(velocity).to(equal(11))
} else {
    fail("Expected <European> but got <\(answer)>")
}

This works, but in my opinion it has two drawbacks:

First, if you want to verify more than one associated value (for example the enum could wrap a complex struct) then you will have multiple expect(..) statements in the body of the if. The fail call at the end in the else branch will be many lines away from its corresponding expectation expressed in the pattern. In the event of a failure you have to scan the test code to find out what has been tested actually. Even worse, the message could be outdated (if you have refactored the pattern, but have forgotten the message some lines below).

Secondly, you actually don't want any control statements like if or switch in your test code. There will be only one correct result and there are no alternate routes in your test ("if this happens then that is executed" - no, there is no if).

What you really want is a single line containing the expectation and that is where the test should fail.

How to achieve that? Unfortunately, patterns are syntax, not expressions. You cannot pass a pattern to another function. There is no way of writing a function that takes a generic pattern and uses it for whatsoever. Apart from that, we would have to find a way for returning the associated values of the enum.

However, we can write functions for specific patterns.

In Nimble the way to do this is writing matchers. Matchers are functions that return a Nimble MatcherFunc, which returns a Bool (true if it matches, false if it doesn't). A matcher can be passed to the to or notTo function: expect(expression).to(matcher).

Let's start with a matcher beEuropean that expects a European swallow having a velocity that equals the given expected parameter:

func beEuropean(expected: Int) -> MatcherFunc<SwallowVelocity> {
    return MatcherFunc { expression, message in
        message.postfixMessage = "be European(\(expected))"
        if let actual = try expression.evaluate(),
            case let .European(velocity) = actual {
                return velocity == expected
        }
        return false
    }
}

Admittedly, that seems to be really a lot of code. Much more than the original 5 lines of pattern matching. But: the test itself gets much cleaner. It will pay off as soon as you have more than one test for the same enumeration type. Here is the test:

expect(answer).to(beEuropean(11))

Additionally: if it fails, it is this single line that will be marked as failed.

For simple associated values like Int this will work sufficiently well. For complex values we need a different solution.

Building test pipelines

The main idea is: we can pass a closure instead of a concrete value:

func beEuropean(test: Int -> () = { _ in } ) ->
        MatcherFunc<SwallowVelocity> {
    return MatcherFunc { expression, message in
        message.postfixMessage = "be European"
        if let actual = try expression.evaluate(),
            case let .European(velocity) = actual {
                test(velocity)
                return true
        }
        return false
    }
}

Instead of the expected parameter we have now a test parameter. It has the empty closure as default value, so we don't need to pass additional tests if we don't want to. This version of the MatcherFunc deals only with the pattern matching (it tests for a European swallow) and then calls the given test closure for testing the associated value.

The corresponding test now looks like this:

expect(answer).to(beEuropean() { velocity in
    expect(velocity).to(equal(11))
})

And there is another benefit: more specific failure messages. If we get a different enum (say an African) then the first line fails. If we get an European with a different velocity then the second line fails. Yay!

Do I really have to use Nimble for all of this?

No. Of course you can adapt this idea and write an equivalent version of the helper function that can be invoked within the XCTest framework. It has two parameters, the actual value and the test closure:

func expectEuropean(
    actual: SwallowVelocity,
    test: Int -> () = { _ in } )
{
    guard case let .European(velocity) = actual else {
        XCTFail("expected <European>, got<\(actual)>")
        return
    }
    test(velocity)
}

Looks even simpler than the Nimble matcher function, doesn't it? You can call this function like this:

expectEuropean(answer) { velocity in
    XCTAssertEqual(11, velocity)
}

However, there is still a minor problem. The XCTFail call in expectEuropean reports as the failure location the line in the helper function. Fortunately, we can solve it by providing a different file and line location as parameters:

func expectEuropean(
    actual: SwallowVelocity,
    file: String = __FILE__, line: UInt = __LINE__,
    test: Int -> () = { _ in } )
{
    guard case let .European(velocity) = actual else {
        XCTFail("expected <European>, got<\(actual)>",
                file: file, line: line)
        return
    }
    test(velocity)
}

The values __FILE__ and __LINE__ are magic macros that the preprocessor will replace before compiling the code. By adding them as default values in the parameter list of our helper function we get the location of the caller. Then the new parameters file and line are passed to XCTFail. If we now encounter an African swallow, the call of expectEuropean is reported as the failure location.

Both functions beEuropean and expectEuropean do nearly the same. beEuropean depends on Nimble und is a little bit more complicated, but in return you get lazily computed values and asynchronous expectations.

The final question is, is it worth to add fairly complicated helper functions to your tests to simplify only fives lines of code? Well, it depends. It depends how intensive you are using and testing enums.

Recursive data structures

One use case are recursive data structures using indirect enums, for example a binary tree:This code has been taken from Airspeed Velocity's awesome article «A persistent tree using indirect enums in Swift».

enum Color { case R, B }

indirect enum Tree<Element: Comparable> {
    case Empty
    case Node(Color, Tree<Element>, Element, Tree<Element>)
}

We can create the following helper functions for matching the Empty and Node cases:For simplicity I have ignored the Color of each Node in the tree.

func expectEmpty<T>(actual: Tree<T>,
    file: String = __FILE__, line: UInt = __LINE__)
{
    guard case .Empty = actual else {
        XCTFail("expected <Empty>, got<\(actual)>",
            file: file, line: line)
        return
    }
}

func expectNode<T>(
    actual: Tree<T>,
    file: String = __FILE__, line: UInt = __LINE__,
    test: (Tree<T>, T, Tree<T>) -> () = { _ in } )
{
    guard case let .Node(_, le, val, ri) = actual else {
        XCTFail("expected <Node>, got<\(actual)>",
            file: file, line: line)
        return
    }
    test(le, val, ri)
}

Next, in our test example, we build a binary tree containing the three names Arthur, Lancelot, and Galahad. After that, the test ensures that the tree is balanced correctly, i.e. Galahad is in the root node, to his left is Arthur and to his right is Lancelot:

let knights = Tree(["Arthur", "Lancelot", "Galahad"]);
expectNode(knights) { le, val, ri in
    XCTAssertEqual("Galahad", val)
    expectNode(le) { le, val, ri in
        XCTAssertEqual("Arthur", val)
        expectEmpty(le)
        expectEmpty(ri)
    }
    expectNode(ri) { le, val, ri in
        XCTAssertEqual("Lancelot", val)
        expectEmpty(le)
        expectEmpty(ri)
    }
}

See, how compact the test is. The nesting of the closures mirrors the tree structure. Clean code at its best.

I'm interested in your feedback. Send me a mail or ping me on twitter.