Nimble: a custom matcher for a rescue
This post is not going to explain what is Nimble. I can only say that I and many other iOS devs are using it to express the expected outcomes of Swift expressions in their tests. If you want to learn more, go here.
Nimble Matchers
A Nimble matcher is a function that allows you to describe
- the positive case (the values match),
- the negative case (they don’t match),
- any error cases all in one function.
This means that not only can you write expect(value).to(equal(expectedValue))
, but you can also reuse the same matcher for the negative case expect(value).toNot(equal(exptectedValue))
. Nice, right?
Examples:
import Nimble
expect(1 + 1).to(equal(2))
// success - expected to equal <2>, and got <2>
expect(1+1).toNot(equal(3))
// success - expected <2>, to not be equal <2>
Let’s, have a look at how such a matcher looks in code. Btw, here is the list of already defined matchers.
In Nimble, the matchers are Swift functions that take an expected value and return a Predicate
closure. The return value of a Predicate
closure is a PredicateResult
that indicates whether the actual value matches the expectation and what error message to display on failure. The expression represents the closure of the value inside expect(...)
.
Predicate
public struct Predicate<T> {
fileprivate var matcher: (Expression<T>) throws -> PredicateResult
/// Constructs a predicate that knows how take a given value
public init(_ matcher: @escaping (Expression<T>) throws -> PredicateResult) {
self.matcher = matcher
}
/// Uses a predicate on a given value to see if it passes the predicate.
///
/// @param expression The value to run the predicate's logic against
/// @returns A predicate result indicate passing or failing and an associated error message.
public func satisfies(_ expression: Expression<T>) throws -> PredicateResult {
return try matcher(expression)
}
}
PredicateResult
It is the return struct that Predicate
return to indicate success or failure. A PredicateResult
is made up of two values:
- PredicateStatus
- ExpectationMessage
PredicateStatus
public enum PredicateStatus {
// The predicate "passes" with the given expression
// eg - expect(1).to(equal(1))
case matches
// The predicate "fails" with the given expression
// eg - expect(1).toNot(equal(1))
case doesNotMatch
// The predicate never "passes" with the given expression, even if negated
// eg - expect(nil as Int?).toNot(equal(1))
case fail
// ...
}
ExpectationMessage
It provides messaging semantics for error reporting.
public indirect enum ExpectationMessage {
// Emits standard error message:
// eg - "expected to <string>, got <actual>"
case expectedActualValueTo(/* message: */ String)
// Allows any free-form message
// eg - "<string>"
case fail(/* message: */ String)
// ...
}
Predicates should usually depend on either .expectedActualValueTo(..)
or .fail(..)
when reporting errors.
Custom matchers in practice
There are a few ways how we can init our own matchers but I will focus on two which I use the most:
/// Defines a predicate with a default message that can be returned in the closure
/// Also ensures the predicate's actual value cannot pass with `nil` given.
public static func define(_ message: String = "match", matcher: @escaping (Expression<T>, ExpectationMessage) throws -> PredicateResult) -> Predicate<T>
/// Provides a simple predicate definition that provides no control over the predefined /// error message. Also ensures the predicate's actual value cannot pass with `nil` given.
public static func simple(_ message: String = "match", matcher: @escaping (Expression<T>) throws -> PredicateStatus) -> Predicate<T>
Let’s say that we have an enum that describes http method with some associated values:
We want to have the matcher/s which allows us to write a neat test. First, a get matcher for which we will use a define
method.
Example of usage:
For the second example, we will use simple
method:
Example of usage:
Summary
As we can see in the above examples. Writing our own Nimble matcher requires us to write more code than the pattern-matching solution. However, in the end, the test is much more flexible when it comes to checks which we want to make and is easier to read.