Testing requirements with #require in Swift Testing
Published on: November 28, 2024In a previous post, I wrote about using the #expect
macro to ensure that certain assertions you want to make about your code are true. We looked at testing boolean conditions as well as errors.
In this post, I would like to take a look at a macro that goes hand-in-hand with #expect
and that is the #require
macro.
The #require
macro is used to ensure that certain conditions in your test are met, and to abort your test if these conditions are not met. The key difference between #expect
and #require
is that #expect
will not cause a failed assertion to stop the test.
#require
is much stricter. If we find one assertion to be untrue inside of the #require
macro, we end the test because we don't think it makes sense to test any further.
In this post, we'll take a look at several applications of the #require
macro. For example, we'll use #require
to ensure that an optional value can be unwrapped. We'll also see how you can use #require
to ensure that a specific error is or is not thrown. And of course, we'll also look at boolean conditions inside of #require
.
Let's start by looking at Optional
.
Unwrapping optionals with #require
Sometimes in our code we will have optional values. They're pretty much unavoidable in Swift and they're actually a really useful tool. In your test, it is quite likely that you'll want to make sure that a certain value exists before proceeding with your test. One way to do this would be to use the #expect
macro and ensure that some property or value is not nil
.
However, sometimes you'll want to take your optional value and use it as input for something else or you want to do further testing with that object. In that case, it makes sense to abort your test entirely if the optional happens to be nil
.
We can use the #require
macro for this, here’s how:
@Test func userIsReturned() async throws {
let userStore = UserInfoStore()
let user = User(name: "John")
userStore.addUser(user: user)
let returnedUser = try #require(userStore.getUser(withName: "John"), "User store should return the user that was added")
#expect(returnedUser == user, "User store should return the user that was added")
}
The magic here is on the line where we create our let returnedUser
. We use the #require
macro and we call it with the try
keyword.
That's because if the #require
macro fails to unwrap the optional that is returned by getUser
, the macro will throw an error and so our test will actually fail. This is quite useful when you really don't want to continue your test if whatever you're trying to require isn't there.
So in this case I want to compare the return user with the one that I've tried to store. I cannot do that if the user isn't there. So I want my test to not just fail when the optional that's returned by getUser
is nil
, I want this test case to end.
Now let’s imagine that I also want to end my test if the returned user and the stored user aren’t the same…
Checking boolean conditions with #require
In the previous section I used the following to line to make sure that my getUser
function returned the correct user:
#expect(returnedUser == user, "User store should return the user that was added")
Notice how I'm using #expect
to compare my returned user to my stored user.
This expectation will allow my test to continue running even if the expectation fails. This would allow me to perform multiple assertions on an object. For example, if I were to check whether the user name, the user's ID, and a bunch of other properties match, I would use #expect
so that I can perform all assertions and see exactly which ones failed.
In this case I would want my test to fail and end if I didn’t get the right user back.
So I'm comparing the two users like before and I’ve replaced my #expect
with #require
. Here's what that looks like in a full test.
@Test func userIsReturned() async throws {
let userStore = UserInfoStore()
let user = User(name: "John")
userStore.addUser(user: user)
let returnedUser = try #require(userStore.getUser(withName: "John"), "User store should return the user that was added")
try #require(returnedUser == user, "User store should return the user that was added")
print("this won't run if I got the wrong user")
}
Notice that I had to prefix my #require
with the try
keyword, just like I had for getting my returned user on the line before.
The reason for that is if I didn't get the right user back and it doesn't match with the user that I just stored, my test will throw an error and end with a failure.
Overall, the APIs for #require
and #expect
are pretty similar, with the key difference being that #require
needs the try
keyword and your test ends if a requirement isn't met.
Now that we've seen how we can use this to unwrap optionals and check boolean conditions, the next step is to see how we can use it to check for certain errors being thrown.
Checking errors with #require
If you know how to check for errors with the #expect
macro, you basically know how to it do with the #require
macro too.
The key difference being once again if a requirement is not met your test case will stop.
If you want to learn more about checking for errors, I urge you to take a look at my blog post on the #expect
macro. I don't want to duplicate everything that's in there in this post, so for an in-depth overview, you can take a look at that post.
In this post, I would just like to give you a brief rundown of what it looks like to check for errors with the #require
macro.
So first let's see how we can assert that certain function throws an expected error with the #require
macro.
I will be using the same example that I used in the previous post. We're going to check that giving an incorrect input to an object will actually throw the error that I want to receive.
@Test func errorIsThrownForIncorrectInput() async throws {
let input = -1
try #require(throws: ValidationError.valueTooSmall(margin: 1), "Values between 0 and 100 should be okay") {
try checkInput(input)
}
}
In this specific example, it might not make a ton of sense to use #require
over #expect
. However, if I were to have more code after this assertion and it wouldn't make sense to continue my test if the wrong error was thrown, then it makes total sense for me to use #require
because I want to abandon the test because there's no point in continuing on.
Similar to the #expect
macro, we can pass a specific error (like I did in the example above) or an error type (like ValidationError.self
). If we want to assert that no error is thrown, we could pass Never.self
as the error. type to make sure that our function call does not throw.
Similar to the #expect
macro, you can use the #require
macro to check whether a certain expression throws an error based on a more complicated evaluation.
For all the different overloads that exist on #require
, I would like to redirect you to the #expect
macro post because they are exactly the same for #require
and #expect
. The key difference is what happens when the assertion fails: #expect
will allow your test to continue, but it will fail with an error on the line where your assertion failed. With #require
, your test case will simply end on the line where something that you didn't expect actually happened.
In Summary
Overall, I quite like that Swift testing allows us to have a loose checking for assertions in the #expect
macro, where we can validate that certain things are or are not correct without failing the entire test. That would allow you to make a whole bunch of assertions and see which ones fail, fixing one problem at a time (running your test again, fixing the next problem that shows up) is tedious.
The #require
macro is really nice when you pretty much rely on something to be returned or something to be true before you can proceed.
For example, unwrapping an optional if you want to use whatever you're trying to unwrap to run further code and perform further assertions. It makes no sense to continue your test because you know that every single assertion that comes after it will fail, so I really like using #require
for those kinds of situations and #expect
for the ones where I can continue my test to collect more information about the results.