Asserting state with #anticipate in Swift Testing – Donny Wals


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 about your code that you just need to guarantee is right.

For instance, if I had been to jot down a operate 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 operate I get 11 out of it. A testing library that may not be capable to do that isn’t price a lot. And so it ought to be no shock in any respect that Swift testing has a manner for us to carry out assertions.

Swift testing makes use of the #anticipate macro for that.

On this put up, we’re going to check out the #anticipate macro. We’ll get began by utilizing it for a easy Boolean assertion after which work our manner as much as extra advanced assertions that contain errors.

Testing easy boolean circumstances with #anticipate

The commonest manner that you just’re most likely going to be utilizing #anticipate is to guarantee that sure circumstances are evaluated to betrue. For instance, I would need to check that the operate beneath really returns 5 at any time when I name it.

func returnFive() -> Int {
  return 0
}

After all this code is just a little bit foolish, it does not actually do this a lot, however you may think about {that a} extra difficult piece of code would should be examined extra completely.

Since I have never really applied my returnFive operate but, it simply returns 0. What I can do now’s write a check as proven beneath.

@Take a look at func returnFiveWorks() async throws {
  let functionOutput = Incrementer().returnFive()
  #anticipate(5 == functionOutput)
}

This check goes to check that once I name my operate, we get quantity 5 again. Discover the road the place it says #anticipate(5 == functionOutput).

That’s an assertion.

I’m attempting to claim that 5 equals the output of my operate by utilizing the #anticipate macro.

When our operate returns 5, my expression (5 == functionOutput) evaluated to true and the check will move. When the expression is false, the check 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 implies that we will simply see what went fallacious.

We will present extra context to our check failures by including a remark. For instance:

@Take a look at func returnFiveWorks() async throws {
  let functionOutput = Incrementer().returnFive()
  #anticipate(5 == functionOutput, "returnFive() ought to at all times return 5")
}

If we replace our checks to look just a little bit extra like this, if the check fails we are going to see an output that is a little more elaborate (as you’ll be able to see beneath).

Expectation failed: 5 == (functionOutput → 0)
returnFive() ought to at all times return 5

I at all times like to jot down a remark in my expectations as a result of it will present just a little 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 anticipate macro:

  1. The primary argument is at all times going to be a Boolean worth
  2. A remark that will likely be proven upon check failure

So within the check you noticed earlier, I had my comparability between 5 and the operate output within my expectation macro as follows:

5 == functionOutput

If I had been to alter my code to appear to be this the place I put the comparability outdoors of the macro, the output of my failing check goes to look just a little bit completely different. This is what it’ll appear to be:

@Take a look at func returnFiveWorks() async throws {
  let functionOutput = Incrementer().returnFive()
  let didReturnFive = 5 == functionOutput
  #anticipate(didReturnFive, "returnFive() ought to at all times return 5")
}

// produces the next failure message:
// Expectation failed: didReturnFive
// returnFive() ought to at all times return 5

Discover how I am not getting any suggestions proper now about what might need gone fallacious. I merely get a message that claims “Expectation failed: didReturnFive” and no context as to what precisely might need gone fallacious.

I at all times suggest attempting to place your expressions contained in the anticipate macro as a result of that’s merely going to make your check output much more helpful as a result of it’ll examine variables that you just inserted into your anticipate macro and it’ll say “you anticipated 5 however you have obtained 0”.

On this case I solely know that I didn’t get 5, which goes to be loads more durable to debug.

We will even have a number of variables that we’re utilizing within anticipate and have the testing framework inform us about these as effectively.

So think about I’ve a operate the place I enter a quantity and the quantity that I need to increment the quantity by. And I anticipate the operate to carry out the mathematics increment the enter by the quantity given. I might write a check that appears like this.

@Take a look at func incrementWorks() async throws {
  let enter = 1
  let incrementBy = 2
  let functionOutput = Incrementer().increment(enter: enter, by: incrementBy)
  #anticipate(functionOutput == enter + incrementBy, "increment(enter:by:) ought to add the 2 numbers collectively")
}

This check defines an enter variable and the quantity that I need to increment the primary variable by.

It passes them each to an increment operate after which does an assertion that checks whether or not the operate output equals the enter plus the increment quantity. If this check 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 operate returned 4, and that isn’t equal to enter + increment (which is 3). It is actually like this degree of element in my failure messages.

It’s particularly helpful if you pair this with the check arguments that I lined in my put up on parameterized testing. You may simply see a transparent report on what your inputs had been, what the output was, and what could have gone fallacious for every completely different enter worth.

Along with boolean circumstances like we’ve seen to this point, you would possibly need to write checks that examine whether or not or not your operate threw an error. So let’s check out testing for errors utilizing anticipate subsequent.

Testing for errors with #anticipate

Generally, the purpose of a unit check is not essentially to examine that the operate produces the anticipated output, however that the operate produces the anticipated error or that the operate merely does not throw an error. We will use the anticipate macro to claim this.

For instance, I might need a operate that throws an error if my enter is both smaller than zero or bigger than 50. This is what that check might appear to be with the anticipate macro:

@Take a look at func errorIsThrownForIncorrectInput() async throws {
  let enter = -1
  #anticipate(throws: ValidationError.valueTooSmall, "Values lower than 0 ought to throw an error") {
    attempt checkInput(enter)
  }
}

The syntax for the anticipate macro if you’re utilizing it for errors is barely completely different than you would possibly anticipate primarily based on what the Boolean model regarded like. This macro is available in varied flavors, and I want the one you simply noticed for my normal goal error checks.

The primary argument that we move is the error that we anticipate to be thrown. The second argument that we move is the remark that we need to print at any time when one thing goes fallacious. The third argument is a closure. On this closure we run the code that we need to examine thrown errors for.

So for instance on this case I am calling attempt checkInput which implies that I anticipate that code to throw the error that I specified as the primary argument in my #anticipate.

If the whole lot works as anticipated and checkInput throws an error, my check will move so long as that error matches ValidationError.valueTooSmall.

Now to illustrate that I by accident throw a distinct error for this operate the output will look just a little bit like this

Expectation failed: anticipated error "valueTooSmall" of sort ValidationError, however "valueTooLarge" of sort ValidationError was thrown as a substitute
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 #anticipate macro will really inform us what we acquired and what we anticipated, making it straightforward to determine what might have gone fallacious.

Including just a little remark identical to we did with the Boolean model makes it simpler to cause about what we anticipated to occur or what might be occurring.

If the check doesn’t throw an error in any respect, the output would look as proven beneath

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 anticipate an error to be thrown.

There may be conditions the place you do not actually care concerning the precise error being thrown, however simply that an error of a particular sort was thrown. For instance, I may not care that my “worth too small” or “worth too giant” error was thrown, however I do care that the kind of error that obtained thrown was a validation error. I can write my check like this to examine for that.

@Take a look at func errorIsThrownForIncorrectInput() async throws {
  let enter = -1
  #anticipate(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 anticipate to be thrown, I merely move ValidationError.self. This can permit my check to move when any validation error is thrown. If for no matter cause I throw a distinct type of error, the check would fail.

There is a third model of anticipate in relation to errors that we might use. This one would first permit us to specify a remark like we will in any anticipate. We will then move 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 will return whether or not or not that was what we anticipated.

For instance, when you’ve got 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 effectively. This is what that would appear to be.

@Take a look at func errorIsThrownForIncorrectInput() async throws {
  let enter = -1
  #anticipate {
    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 primary, however we might develop this in the true world. That is actually helpful when you’ve gotten a sophisticated error or difficult logic to find out whether or not or not the error was precisely what you anticipated.

Personally, I discover that most often I’ve fairly simple error checking, so I’m usually utilizing the very first model of anticipate that you just noticed on this part. However I’ve undoubtedly dropped right down to this one once I wished to examine extra difficult circumstances to find out whether or not or not I obtained what I anticipated from my error.

What you want is, in fact, going to rely by yourself particular scenario, however know that there are three variations of anticipate that you need to use when checking for errors, and that all of them have kind 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 achieved a extremely good job of offering us with a fairly primary however highly effective sufficient API within the #anticipate macro. There’s additionally the #require macro that we’ll discuss extra in a separate put up, however the #anticipate macro by itself is already a good way to start out writing unit checks. It offers a number of context about what you are doing as a result of it is a macro and it’ll develop into much more info behind the scenes. The API that we write is fairly clear, fairly concise, and it is highly effective to your testing wants.

Ensure that to take a look at this class of Swift testing on my web site as a result of I had a number of completely different posts with Swift testing, and I plan to develop this class over time. If there’s something you need me to speak about when it comes to Swift testing, be sure to discover me on social media, I’d love to listen to from you.

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles