I do not assume I’ve ever heard of a testing library that does not have some mechanism to check assertions. An assertion within the context of testing is basically an assumption that you’ve got about your code that you just need to guarantee is appropriate.
For instance, if I have been to write down a perform that is supposed so as to add one to any given quantity, then I’d need to assert that if I put 10 into that perform I get 11 out of it. A testing library that will not have the ability to do that’s not price a lot. And so it must be no shock in any respect that Swift testing has a method for us to carry out assertions.
Swift testing makes use of the #count on macro for that.
On this publish, we’re going to check out the #count on macro. We’ll get began by utilizing it for a easy Boolean assertion after which work our method as much as extra advanced assertions that contain errors.
Testing easy boolean circumstances with #count on
The most typical method that you just’re most likely going to be utilizing #count on is to guarantee that sure circumstances are evaluated to betrue. For instance, I would need to take a look at that the perform under really returns 5 every time I name it.
func returnFive() -> Int {
return 0
}
After all this code is a little bit bit foolish, it would not actually try this a lot, however you can think about {that a} extra difficult piece of code would must be examined extra completely.
Since I have not really applied my returnFive perform but, it simply returns 0. What I can do now’s write a take a look at as proven under.
@Check func returnFiveWorks() async throws {
let functionOutput = Incrementer().returnFive()
#count on(5 == functionOutput)
}
This take a look at goes to check that after I name my perform, we get quantity 5 again. Discover the road the place it says #count on(5 == functionOutput).
That’s an assertion.
I’m attempting to claim that 5 equals the output of my perform by utilizing the #count on macro.
When our perform returns 5, my expression (5 == functionOutput) evaluated to true and the take a look at will cross. When the expression is false, the take a look at will fail with an error that appears a bit like this:
Expectation failed: 5 == (functionOutput → 0)
This error will present up as an error on the road of code the place the expectation failed. That signifies that we are able to simply see what went incorrect.
We will present extra context to our take a look at failures by including a remark. For instance:
@Check func returnFiveWorks() async throws {
let functionOutput = Incrementer().returnFive()
#count on(5 == functionOutput, "returnFive() ought to all the time return 5")
}
If we replace our assessments to look a little bit bit extra like this, if the take a look at fails we’ll see an output that is a little more elaborate (as you possibly can see under).
Expectation failed: 5 == (functionOutput → 0)
returnFive() ought to all the time return 5
I all the time like to write down a remark in my expectations as a result of this can present a little bit bit extra context about what I anticipated to occur, making debugging my code simpler in the long term.
Typically talking, you are both going to be passing one or two arguments to the count on macro:
- The primary argument is all the time going to be a Boolean worth
- A remark that will probably be proven upon take a look at failure
So within the take a look at you noticed earlier, I had my comparability between 5 and the perform output within my expectation macro as follows:
5 == functionOutput
If I have been to alter my code to appear like this the place I put the comparability exterior of the macro, the output of my failing take a look at goes to look a little bit bit completely different. This is what it would appear like:
@Check func returnFiveWorks() async throws {
let functionOutput = Incrementer().returnFive()
let didReturnFive = 5 == functionOutput
#count on(didReturnFive, "returnFive() ought to all the time return 5")
}
// produces the next failure message:
// Expectation failed: didReturnFive
// returnFive() ought to all the time return 5
Discover how I am not getting any suggestions proper now about what might need gone incorrect. I merely get a message that claims “Expectation failed: didReturnFive” and no context as to what precisely might need gone incorrect.
I all the time suggest attempting to place your expressions contained in the count on macro as a result of that’s merely going to make your take a look at output much more helpful as a result of it would examine variables that you just inserted into your count on macro and it’ll say “you anticipated 5 however you’ve got bought 0”.
On this case I solely know that I didn’t get 5, which goes to be quite a bit more durable to debug.
We will even have a number of variables that we’re utilizing within count on and have the testing framework inform us about these as nicely.
So think about I’ve a perform the place I enter a quantity and the quantity that I need to increment the quantity by. And I count on the perform to carry out the mathematics increment the enter by the quantity given. I may write a take a look at that appears like this.
@Check func incrementWorks() async throws {
let enter = 1
let incrementBy = 2
let functionOutput = Incrementer().increment(enter: enter, by: incrementBy)
#count on(functionOutput == enter + incrementBy, "increment(enter:by:) ought to add the 2 numbers collectively")
}
This take a look at defines an enter variable and the quantity that I need to increment the primary variable by.
It passes them each to an increment perform after which does an assertion that checks whether or not the perform output equals the enter plus the increment quantity. If this take a look at fails, I get an output that appears as follows:
Expectation failed: (functionOutput → 4) == (enter + incrementBy → 3)
increment(enter:by:) ought to add the 2 numbers collectively
Discover how I fairly conveniently see that my perform returned 4, and that’s not equal to enter + increment (which is 3). It is actually like this degree of element in my failure messages.
It’s particularly helpful whenever you pair this with the take a look at arguments that I coated in my publish on parameterized testing. You possibly can simply see a transparent report on what your inputs have been, what the output was, and what might have gone incorrect for every completely different enter worth.
Along with boolean circumstances like we’ve seen to date, you would possibly need to write assessments that test whether or not or not your perform threw an error. So let’s check out testing for errors utilizing count on subsequent.
Testing for errors with #count on
Typically, the aim of a unit take a look at is not essentially to test that the perform produces the anticipated output, however that the perform produces the anticipated error or that the perform merely would not throw an error. We will use the count on macro to claim this.
For instance, I might need a perform that throws an error if my enter is both smaller than zero or bigger than 50. This is what that take a look at may appear like with the count on macro:
@Check func errorIsThrownForIncorrectInput() async throws {
let enter = -1
#count on(throws: ValidationError.valueTooSmall, "Values lower than 0 ought to throw an error") {
attempt checkInput(enter)
}
}
The syntax for the count on macro whenever you’re utilizing it for errors is barely completely different than you would possibly count on based mostly on what the Boolean model seemed like. This macro is available in numerous flavors, and I want the one you simply noticed for my common objective error assessments.
The primary argument that we cross is the error that we count on to be thrown. The second argument that we cross is the remark that we need to print every time one thing goes incorrect. The third argument is a closure. On this closure we run the code that we need to test thrown errors for.
So for instance on this case I am calling attempt checkInput which signifies that I count on that code to throw the error that I specified as the primary argument in my #count on.
If the whole lot works as anticipated and checkInput throws an error, my take a look at will cross so long as that error matches ValidationError.valueTooSmall.
Now for example that I by chance throw a distinct error for this perform the output will look a little bit bit like this
Expectation failed: anticipated error "valueTooSmall" of sort ValidationError, however "valueTooLarge" of sort ValidationError was thrown as an alternative
Values lower than 0 ought to throw an error
Discover how the message explains precisely which error we acquired (valueTooLarge) and the error that we anticipated (valueTooSmall). It is fairly handy that the #count on macro will really inform us what we acquired and what we anticipated, making it straightforward to determine what may have gone incorrect.
Including a little bit remark similar to we did with the Boolean model makes it simpler to purpose about what we anticipated to occur or what may very well be taking place.
If the take a look at doesn’t throw an error in any respect, the output would look as proven under
ExpectMacro.swift:42:3: Expectation failed: an error was anticipated however none was thrown
Values lower than 0 ought to throw an error
This error fairly clearly tells us that no error was thrown whereas we did count on an error to be thrown.
There may be conditions the place you do not actually care concerning the actual error being thrown, however simply that an error of a selected sort was thrown. For instance, I may not care that my “worth too small” or “worth too massive” error was thrown, however I do care that the kind of error that bought thrown was a validation error. I can write my take a look at like this to test for that.
@Check func errorIsThrownForIncorrectInput() async throws {
let enter = -1
#count on(throws: ValidationError.self, "Values lower than 0 ought to throw an error") {
attempt checkInput(enter)
}
}
As an alternative of specifying the precise case on validation error that I count on to be thrown, I merely cross ValidationError.self. It will enable my take a look at to cross when any validation error is thrown. If for no matter purpose I throw a distinct sort of error, the take a look at would fail.
There is a third model of count on in relation to errors that we may use. This one would first enable us to specify a remark like we are able to in any count on. We will then cross a closure that we need to execute (e.g. calling attempt checkInput) and a second closure that receives no matter error we acquired. We will carry out some checks on that after which we are able to return whether or not or not that was what we anticipated.
For instance, in case you have a bit extra difficult setup the place you are throwing an error with an related worth you would possibly need to examine the related worth as nicely. This is what that would appear like.
@Check func errorIsThrownForIncorrectInput() async throws {
let enter = -1
#count on {
attempt checkInput(enter)
} throws: { error in
guard let validationError = error as? ValidationError else {
return false
}
swap validationError {
case .valueTooSmall(let margin) the place margin == 1:
return true
default:
return false
}
}
}
On this case, our validation logic for the error is fairly fundamental, however we may increase this in the true world. That is actually helpful when you might have an advanced error or difficult logic to find out whether or not or not the error was precisely what you anticipated.
Personally, I discover that normally I’ve fairly easy error checking, so I’m typically utilizing the very first model of count on that you just noticed on this part. However I’ve positively dropped right down to this one after I needed to examine extra difficult circumstances to find out whether or not or not I bought what I anticipated from my error.
What you want is, after all, going to rely by yourself particular state of affairs, however know that there are three variations of count on that you should utilize when checking for errors, and that all of them have form of their very own downsides that you just would possibly need to keep in mind.
In Abstract
Often, I consider testing libraries by how highly effective or expressive their assertion APIs are. Swift Testing has finished a extremely good job of offering us with a reasonably fundamental however highly effective sufficient API within the #count on macro. There’s additionally the #require macro that we’ll speak about extra in a separate publish, however the #count on macro by itself is already an effective way to begin writing unit assessments. It gives a whole lot of context about what you are doing as a result of it is a macro and it’ll increase into much more data behind the scenes. The API that we write is fairly clear, fairly concise, and it is highly effective on your testing wants.
Be certain that to take a look at this class of Swift testing on my web site as a result of I had a whole lot of completely different posts with Swift testing, and I plan to increase this class over time. If there’s something you need me to speak about by way of Swift testing, be sure you discover me on social media, I’d love to listen to from you.
