Say you have this enum:
enum State {
case inital
case content(Response)
}
If you are familiar with Nimble, the first thing you may try is:
expect(state).to(equal(State.content(expectedResponse)))
But in Swift, when an enumeration has associated values (here a Response
object for the .content
case) it does not implement the Equatable
protocol by default. If the object is not very complex you could implement it and carry on, but if the object entails some complexity, implementing the protocol may be quite time-consuming. Thankfully this will be much easier in future versions of Swift.
One way to do it would be using pattern matching:
if case let .content(response) = state {
expect(response.foo).to(equal(expectedResponse.foo))
} else {
fail("Expected <content> but got <\(state)>")
}
Wrap this with a function and it’s ready to use in our tests. Although this works, it can be better.
To begin with, it’s not the way we usually write tests. When asserting the results of a test, we usually look for the result to have a specific value. We should not have different paths of assertions, so we always avoid using an if-else
statement in our test code.
Secondly, if we are verifying several properties of the object, the block might become quite large making it a bit harder to read the whole if-else
statement. Maybe in the future we change the enum’s name, and the if
block is a couple of lines long, we might overlook the fail
statement and forget to update the message.
Finally, our test code will not look uniform if we have a couple of expectations using Nimble and then we have that pattern matching block or a call to the wrapper function.
We can improve this writing our own matchers for Nimble. A matcher is a Swift function that returns a Predicate
closure. The matcher for the .content
enum would be the following:
private func beContent(test: @escaping (Response) -> Void = { _ in }) -> Predicate<State> {
return Predicate.define("be <content>") { expression, message in
if let actual = try expression.evaluate(),
case let .content(response) = actual {
test(response)
return PredicateResult(status: .matches, message: message)
}
return PredicateResult(status: .fail, message: message)
}
}
The .define
method takes a message as the first parameter, which will be shown when the assertion fails: expected to be content, got <initial>.
Then, with a trailing closure, we define the matcher. We get the actual value and using pattern matching we check if it’s the enum we’re looking for. If it is so, we call an escaping closure that by default does nothing, where we can do whatever checks we need on the associated value, and finally return the result of the predicate using PredicateResult
with the .matches
parameter.
If we did not match the enum we wanted, we return a PredicateResult
with the .fail
parameter.
We then can use our own custom matcher like this:
// Just check the enum
expect(state).to(beContent())
// Check the enum and verify whatever properties we need from the associated value
expect(state).to(beContent { response in
expect(response) === expectedResponse
})
For the .initial
enum the matcher is a bit easier as it does not have an associated value:
private func beInitial() -> Predicate<SearchTableViewController.State> {
return Predicate.define("be <initial>") { expression, message in
if let actual = try expression.evaluate(), case .initial = actual {
return PredicateResult(status: .matches, message: message)
}
return PredicateResult(status: .fail, message: message)
}
}
Although writing our own Nimble matcher requires us to write more code than the pattern matching solution, using it on our test methods will result in more homogeneous code with the rest of our assertions and have some flexibility being able to customize the checks made to the associated value on a per test basis using the closure passed by parameter.