Nimble provides two ways to assert code that runs asynchronously, toEventually
and waitUntil
. In this post we look at them in more details, and understand when to use one or the other.
toEventually
You can use toEventually
to write expectations that should be tested at some point in the future.
Its documentation reads:
Tests the actual value using a matcher to match by checking continuously at each pollInterval until the timeout is reached.
It can be used like:
expect(foo).toEventually(equal("something"))
expect(array).toEventually(beEmpty())
toEventually
is very nice because it allows us to write expectations that read like english. "Expect value to eventually be this".
Having code, whether it is production or test, that reads well makes working in the codebase easy.
waitUntil
waitUntil
is not a matcher or an expectation, but just an utility function provided by Nimble.
Its documentation reads:
Wait asynchronously until the done closure is called or the timeout has been reached.
It can be used like:
waitUntil { done in
service.asynMethodWithCallback { value in
// some expectation(s)
// ...
done()
}
}
If done
is not called the test will fail, so remember that will ya?
When to use which
When testing that a certain condition should be met as a result of the invocation of async code toEventually
is your go to API.
Here's a more detailed example, which you can find in full on GitHub.
Imagine we have an AsyncService
, with a method that changes the value of a property on its delegate.
protocol AsyncServiceDelegate: class {
var aProperty: String { get set }
}
class AsyncService {
weak var delegate: AsyncServiceDelegate?
func callThatResultsInSideEffect() {
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 0.1) { [weak self] in
self?.delegate?.aProperty = "bazinga"
}
}
}
A good way to test the implementation of callThatResultsInSideEffect
is to create a fake delegate, pass it to the service, call the method, and finally verify that the value eventually matches our expectation.
class FakeDelegate: AsyncServiceDelegate {
var aProperty: String = "unset"
}
func testToEventually() {
let delegate = FakeDelegate()
let service = AsyncService()
service.delegate = delegate
service.callThatResultsInSideEffect()
expect(delegate.aProperty).toEventually(equal("bazinga"))
}
Let's now add a new asynchronous function to our service. One that takes a callback as input and will call it once the async work is done. For the sake of the example let's imagine that this async work is fetching a certain String
, and the callback receives a Result
type (have a look at this library for a proper implementation).
extension AsyncService {
func doStuff(_ completion: @escaping (Result<String>) -> ()) {
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 0.1) {
completion(Result<String>.success("bazinga"))
}
}
}
To test that doStuff(_ completion:)
behaves properly using toEventually
we could writing something like:
func testDoStuffWithToEventually() {
let service = AsyncService()
var callbackValue: String? = .none
service.doStuff { result in
switch result {
case .success(let value): callbackValue = value
case .error(let error): fail("Expected call to doStuff to succeed.")
}
}
expect(callbackValue).toEventually(equal("bazinga"))
}
There are two issues with this test.
The first is that it requires a bit of extra setup, in the form of the variable callbackValue
. This is not a big deal, but we should strive to keep our tests as lean as possible.
In our case the callback has only one parameter, but it is not uncommon to have async callbacks with more than that. For example URLSession
's dataTask(with:completionHandler:)
has three.
Using this technique we might have to define a number of variables to contain the values received by the callback. This extra work makes the test harder to write and maintain.
The second problem is that the test doesn't read well. "setup, define a variable, make async call with callback that says to copy the received value, expect copy of received value to eventually equal 'bazinga'".
In a case like this using waitUntil
results in a better test, where with better we mean with less setup needed, and reading closer to natural language.
func testAsyncCallResult() {
let service = AsyncService()
waitUntil { done in
service.doStuff { result in
switch result {
case .success(let value):
expect(value) == "bazinga"
done()
case .error:
fail("Expected call to doStuff to suceeded, but it failed")
}
}
}
}
This code, while having a bit more indentation, reads better. "setup, wait until async call returns a result, expecting the returned value to equal 'bazinga'". More straightforward and self explanatory, and arguably cleaner than having to copy received values.
To recap, when the behavior to test is simply the fact that the a value will change as the result of an async call, or that the given callback is actually exectuted, toEventually provides a clean API, that reads like english.
When the behaviour to test is more complex, for example involving asserting the value provided to the callback, or the fact that the success/failure one is called rather than the other, then waitUntil is most suited.
Get in touch oun Twitter @mokagio or leave a comment below if you have any question regarding this post, or if you want to share your strategy for testing asynchronous code using Nimble.
Until next time:
👋 Leave the codebase better than you found it.