Printed on: December 4, 2024
Swift’s new fashionable testing framework is completely pushed by asynchronous code. Because of this all of our check capabilities are async and that we’ve to ensure that we carry out all of our assertions “synchronously”.
This additionally implies that completion handler-based code shouldn’t be as easy to check as code that leverages structured concurrency.
On this put up, we’ll discover two approaches that may be helpful if you’re testing code that makes use of callbacks or completion handlers in Swift Testing.
First, we’ll take a look at the built-in affirmation methodology from the Swift Testing framework and why it won’t be what you want. After that, we’ll take a look at leveraging continuations in your unit checks to check completion handler primarily based code.
Testing async code with Swift Testing’s confirmations
I’ll begin this part by stating that the primary cause that I’m masking affirmation is that it’s current within the framework, and Apple suggests it as an possibility for testing async code. As you’ll be taught on this part, affirmation is an API that’s largely helpful in particular eventualities that, in my expertise, don’t occur all that always.
With that stated, let’s see what affirmation can do for us.
Typically you may write code that runs asynchronously and produces occasions over time.
For instance, you might need a little bit of code that performs work in numerous steps, and through that work, sure progress occasions must be despatched down an AsyncStream.
As normal with unit testing, we’re not going to actually care concerning the actual particulars of our occasion supply mechanism.
In actual fact, I’ll present you ways that is accomplished with a closure as a substitute of an async for loop. Ultimately, the main points right here don’t matter. The primary factor that we’re curious about proper now could be that we’ve a course of that runs and this course of has some mechanism to tell us of occasions whereas this course of is occurring.
Listed here are a number of the guidelines that we wish to check:
- Our object has an
asyncmethodology known ascreateFilethat kicks of a course of that entails a number of steps. As soon as this methodology completes, the method is completed too. - The article additionally has a property
onStepCompletedthat we will assign a closure to. This closure is named for each accomplished step of our course of.
The onStepCompleted closure will obtain one argument; the finished step. This will likely be a worth of kind FileCreationStep:
enum FileCreationStep {
case fileRegistered, uploadStarted, uploadCompleted
}
With out affirmation, we will write our unit check for this as follows:
@Check("File creation ought to undergo all three steps earlier than finishing")
func fileCreation() async throws {
var completedSteps: [FileCreationStep] = []
let supervisor = RemoteFileManager(onStepCompleted: { step in
completedSteps.append(step)
})
strive await supervisor.createFile()
#count on(completedSteps == [.fileRegistered, .uploadStarted, .uploadCompleted])
}
We are able to additionally refactor this code and leverage Apple’s affirmation method to make our check look as follows:
@Check("File creation ought to undergo all three steps earlier than finishing")
func fileCreation() async throws {
strive await affirmation(expectedCount: 3) { affirm in
var expectedSteps: [FileCreationStep] = [.fileRegistered, .uploadStarted, .uploadCompleted]
let supervisor = RemoteFileManager(onStepCompleted: { step in
#count on(expectedSteps.removeFirst() == step)
affirm()
})
strive await supervisor.createFile()
}
}
As I’ve stated within the introduction of this part; affirmation‘s advantages are usually not clear to me. However let’s go over what this code does…
We name affirmation and we offer an anticipated variety of instances we would like a affirmation occasion to happen.
Be aware that we name the affirmation with strive await.
Because of this our check won’t full till the decision to our affirmation completes.
We additionally move a closure to our affirmation name. This closure receives a affirm object that we will name for each occasion that we obtain to sign an occasion has occurred.
On the finish of my affirmation closure I name strive await supervisor.createFile(). This kicks off the method and in my onStepCompleted closure I confirm that I’ve acquired the fitting step, and I sign that we’ve acquired our occasion by calling affirm.
Right here’s what’s attention-grabbing about affirmation although…
We should name the affirm object the anticipated variety of instances earlier than our closure returns.
Because of this it’s not usable if you wish to check code that’s absolutely completion handler primarily based since that might imply that the closure returns earlier than you possibly can name your affirmation the anticipated variety of instances.
Right here’s an instance:
@Check("File creation ought to undergo all three steps earlier than finishing")
func fileCreationCompletionHandler() async throws {
await affirmation { affirm in
let expectedSteps: [FileCreationStep] = [.fileRegistered, .uploadStarted, .uploadCompleted]
var receivedSteps: [FileCreationStep] = []
let supervisor = RemoteFileManager(onStepCompleted: { step in
receivedSteps.append(step)
})
supervisor.createFile {
#count on(receivedSteps == expectedSteps)
affirm()
}
}
}
Discover that I’m nonetheless awaiting my name to affirmation. As an alternative of 3 I move no anticipated rely. Because of this our affirm ought to solely be known as as soon as.
Within the closure, I’m operating my completion handler primarily based name to createFile and in its completion handler I test that we’ve acquired all anticipated steps after which I name affirm() to sign that we’ve carried out our completion handler primarily based work.
Sadly, this check won’t work.
The closure returns earlier than the completion handler that I’ve handed to createFile has been known as. Because of this we don’t name affirm earlier than the affirmation’s closure returns, and that ends in a failing check.
So, let’s check out how we will change this in order that we will check our completion handler primarily based code in Swift Testing.
Testing completion handlers with continuations
Swift concurrency comes with a function known as continuations. In case you are not accustomed to them, I might extremely suggest that you just learn my put up the place I’m going into how you need to use continuations. For the rest of this part, I’ll assume that continuations fundamentals. I’ll simply take a look at how they work within the context of Swift testing.
The issue that we’re attempting to unravel is actually that we are not looking for our check perform to return till our completion handler primarily based code has absolutely executed. Within the earlier part, we noticed how utilizing a affirmation would not fairly work as a result of the affirmation closure returns earlier than the file managers create file finishes its work and calls its completion handler.
As an alternative of a affirmation, we will have our check await a continuation. Within the continuation, we will name our completion handler primarily based APIs after which resume the continuation when our callback is named and we all know that we have accomplished all of the work that we have to do. Let’s have a look at what that appears like in a check.
@Check("File creation ought to undergo all three steps earlier than finishing")
func fileCreationCompletionHandler() async throws {
await withCheckedContinuation { continuation in
let expectedSteps: [FileCreationStep] = [.fileRegistered, .uploadStarted, .uploadCompleted]
var receivedSteps: [FileCreationStep] = []
let supervisor = RemoteFileManager(onStepCompleted: { step in
receivedSteps.append(step)
})
supervisor.createFile {
#count on(receivedSteps == expectedSteps)
continuation.resume(returning: ())
}
}
}
This check seems similar to the check that you just noticed earlier than, however as a substitute of ready for a affirmation, we’re now calling the withCheckedContinuation perform. Within the closure that we handed to that perform, we carry out the very same work that we carried out earlier than.
Nonetheless, within the createFile perform’s completion handler, we resume the continuation solely after we have made positive that the acquired steps from our onStepCompleted closure match with the steps to be anticipated.
So we’re nonetheless testing the very same factor, however this time our check is definitely going to work. That is as a result of the continuation will droop our check till we resume the continuation.
If you’re testing completion handler primarily based code, I normally discover that I’ll attain for this as a substitute of reaching for a affirmation as a result of a affirmation doesn’t work for code that doesn’t have one thing to await.
In Abstract
On this put up, we explored the variations between continuations and confirmations for testing asynchronous code.
You’ve got realized that Apple’s really useful method for testing closure primarily based asynchronous code is with confirmations. Nonetheless, on this put up, we noticed that we’ve to name our affirm object earlier than the affirmation closure returns, in order that implies that we have to have one thing asynchronous that we await for, which is not all the time the case.
Then I confirmed you that if you wish to check a extra conventional completion handler primarily based API, which might be what you are going to be doing, you have to be utilizing continuations as a result of these enable our checks to droop.
We are able to resume a continuation when the asynchronous work that we have been ready for is accomplished and we’ve asserted the outcomes of our asynchronous work are what we’d like them to be utilizing the #count on or #require macros.
