Revealed on: December 4, 2024
Swift’s new fashionable testing framework is fully pushed by asynchronous code. Which means that all of our check capabilities are async and that we’ve got to guarantee that we carry out all of our assertions “synchronously”.
This additionally signifies that completion handler-based code shouldn’t be as simple 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 have a look at the built-in affirmation
methodology from the Swift Testing framework and why it may not be what you want. After that, we’ll have a look at leveraging continuations in your unit assessments to check completion handler based mostly code.
Testing async code with Swift Testing’s confirmations
I’ll begin this part by stating that the principle motive that I’m overlaying 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 usually.
With that mentioned, let’s see what affirmation
can do for us.
Generally you may write code that runs asynchronously and produces occasions over time.
For instance, you may need a little bit of code that performs work in varied steps, and through that work, sure progress occasions must be despatched down an AsyncStream
.
As standard with unit testing, we’re not going to actually care in regards to the actual particulars of our occasion supply mechanism.
In actual fact, I’ll present you the way that is completed with a closure as an alternative of an async for loop. In the long run, the small print right here don’t matter. The principle factor that we’re excited about proper now could be that we’ve got 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
async
methodology referred to ascreateFile
that 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
onStepCompleted
that we will assign a closure to. This closure known as for each accomplished step of our course of.
The onStepCompleted
closure will obtain one argument; the finished step. This might be a worth of sort FileCreationStep
:
enum FileCreationStep {
case fileRegistered, uploadStarted, uploadCompleted
}
With out affirmation
, we will write our unit check for this as follows:
@Take a look at("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()
#anticipate(completedSteps == [.fileRegistered, .uploadStarted, .uploadCompleted])
}
We are able to additionally refactor this code and leverage Apple’s affirmation
strategy to make our check look as follows:
@Take a look at("File creation ought to undergo all three steps earlier than finishing")
func fileCreation() async throws {
strive await affirmation(expectedCount: 3) { verify in
var expectedSteps: [FileCreationStep] = [.fileRegistered, .uploadStarted, .uploadCompleted]
let supervisor = RemoteFileManager(onStepCompleted: { step in
#anticipate(expectedSteps.removeFirst() == step)
verify()
})
strive await supervisor.createFile()
}
}
As I’ve mentioned within the introduction of this part; affirmation
‘s advantages usually are not clear to me. However let’s go over what this code does…
We name affirmation
and we offer an anticipated variety of occasions we would like a affirmation occasion to happen.
Observe that we name the affirmation
with strive await
.
Which means that our check won’t full till the decision to our affirmation
completes.
We additionally go a closure to our affirmation
name. This closure receives a verify
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 best step, and I sign that we’ve acquired our occasion by calling verify
.
Right here’s what’s attention-grabbing about affirmation
although…
We should name the verify
object the anticipated variety of occasions earlier than our closure returns.
Which means that it’s not usable if you wish to check code that’s totally completion handler based mostly since that might imply that the closure returns earlier than you’ll be able to name your affirmation
the anticipated variety of occasions.
Right here’s an instance:
@Take a look at("File creation ought to undergo all three steps earlier than finishing")
func fileCreationCompletionHandler() async throws {
await affirmation { verify in
let expectedSteps: [FileCreationStep] = [.fileRegistered, .uploadStarted, .uploadCompleted]
var receivedSteps: [FileCreationStep] = []
let supervisor = RemoteFileManager(onStepCompleted: { step in
receivedSteps.append(step)
})
supervisor.createFile {
#anticipate(receivedSteps == expectedSteps)
verify()
}
}
}
Discover that I’m nonetheless awaiting my name to affirmation
. As a substitute of 3
I go no anticipated depend. Which means that our verify
ought to solely be referred to as as soon as.
Within the closure, I’m working my completion handler based mostly name to createFile
and in its completion handler I examine that we’ve acquired all anticipated steps after which I name verify()
to sign that we’ve carried out our completion handler based mostly work.
Sadly, this check won’t work.
The closure returns earlier than the completion handler that I’ve handed to createFile
has been referred to as. Which means that we don’t name verify
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 based mostly code in Swift Testing.
Testing completion handlers with continuations
Swift concurrency comes with a characteristic referred to as continuations. In case you are not accustomed to them, I might extremely advocate that you simply learn my put up the place I’m going into how you should utilize continuations. For the rest of this part, I’ll assume that you already know continuations fundamentals. I’ll simply have a look at how they work within the context of Swift testing.
The issue that we’re making an attempt to resolve is basically that we don’t want our check operate to return till our completion handler based mostly code has totally executed. Within the earlier part, we noticed how utilizing a affirmation
does 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 a substitute of a affirmation
, we will have our check look forward to a continuation
. Within the continuation
, we will name our completion handler based mostly APIs after which resume the continuation when our callback known as and we all know that we have completed all of the work that we have to do. Let’s examine what that appears like in a check.
@Take a look at("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 {
#anticipate(receivedSteps == expectedSteps)
continuation.resume(returning: ())
}
}
}
This check appears similar to the check that you simply noticed earlier than, however as an alternative of ready for a affirmation, we’re now calling the withCheckedContinuation
operate. Within the closure that we handed to that operate, we carry out the very same work that we carried out earlier than.
Nonetheless, within the createFile
operate’s completion handler, we resume the continuation solely after we have made certain 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.
Whenever you’re testing completion handler based mostly code, I often discover that I’ll attain for this as an alternative 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 discovered that Apple’s really useful strategy for testing closure based mostly asynchronous code is with confirmations
. Nonetheless, on this put up, we noticed that we’ve got to name our verify
object earlier than the affirmation closure returns, in order that signifies 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 based mostly API, which might be what you are going to be doing, you ought to be utilizing continuations
as a result of these enable our assessments to droop.
We are able to resume a continuation
when the asynchronous work that we had 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 #anticipate
or #require
macros.