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