Asserting state with #anticipate in Swift Testing – Donny Wals

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 simply wish to guarantee is appropriate.

For instance, if I have been to jot down a perform that is supposed so as to add one to any given quantity, then I might wish to assert that if I put 10 into that perform I get 11 out of it. A testing library that may not be capable of do that’s not value a lot. And so it ought to be no shock in any respect that Swift testing has a approach for us to carry out assertions.

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

On this submit, we’re going to try the #anticipate macro. We’ll get began by utilizing it for a easy Boolean assertion after which work our approach as much as extra complicated assertions that contain errors.

Testing easy boolean circumstances with #anticipate

The most typical approach that you simply’re most likely going to be utilizing #anticipate is to ensure that sure circumstances are evaluated to betrue. For instance, I would wish to take a look at that the perform beneath truly returns 5 each time I name it.

func returnFive() -> Int {
  return 0
}

In fact this code is slightly bit foolish, it does not actually do this a lot, however you may think about {that a} extra difficult piece of code would must be examined extra completely.

Since I have never truly applied my returnFive perform but, it simply returns 0. What I can do now could be write a take a look at as proven beneath.

@Check func returnFiveWorks() async throws {
  let functionOutput = Incrementer().returnFive()
  #anticipate(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 #anticipate(5 == functionOutput).

That’s an assertion.

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

When our perform returns 5, my expression (5 == functionOutput) evaluated to true and the take a look at will go. 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 unsuitable.

We are able to 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()
  #anticipate(5 == functionOutput, "returnFive() ought to all the time return 5")
}

If we replace our assessments to look slightly bit extra like this, if the take a look at fails we are going to see an output that is a little more elaborate (as you may see beneath).

Expectation failed: 5 == (functionOutput → 0)
returnFive() ought to all the time return 5

I all the time like to jot down a remark in my expectations as a result of this may present slightly bit extra context about what I anticipated to occur, making debugging my code simpler in the long term.

Usually talking, you are both going to be passing one or two arguments to the anticipate macro:

  1. The primary argument is all the time going to be a Boolean worth
  2. 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 inside 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 outdoors of the macro, the output of my failing take a look at goes to look slightly bit completely different. This is what it can appear like:

@Check func returnFiveWorks() async throws {
  let functionOutput = Incrementer().returnFive()
  let didReturnFive = 5 == functionOutput
  #anticipate(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 may need gone unsuitable. I merely get a message that claims “Expectation failed: didReturnFive” and no context as to what precisely may need gone unsuitable.

I all the time suggest attempting to place your expressions contained in the anticipate 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 can examine variables that you simply inserted into your anticipate macro and it’ll say “you anticipated 5 however you’ve got acquired 0”.

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

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

So think about I’ve a perform the place I enter a quantity and the quantity that I wish to increment the quantity by. And I anticipate the perform to carry out the maths increment the enter by the quantity given. I might 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)
  #anticipate(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 wish 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 stage of element in my failure messages.

It’s particularly helpful whenever you pair this with the take a look at arguments that I lined in my submit 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 unsuitable for every completely different enter worth.

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

Testing for errors with #anticipate

Typically, the objective of a unit take a look at is not essentially to examine that the perform produces the anticipated output, however that the perform produces the anticipated error or that the perform merely does not throw an error. We are able to use the anticipate macro to say this.

For instance, I may 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 might appear like with the anticipate macro:

@Check 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 whenever 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 numerous flavors, and I want the one you simply noticed for my common goal error assessments.

The primary argument that we go is the error that we anticipate to be thrown. The second argument that we go is the remark that we wish to print each time one thing goes unsuitable. The third argument is a closure. On this closure we run the code that we wish to examine thrown errors for.

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

If all the things works as anticipated and checkInput throws an error, my take a look at will go so long as that error matches ValidationError.valueTooSmall.

Now for instance that I by accident throw a unique error for this perform the output will look slightly bit like this

Expectation failed: anticipated error "valueTooSmall" of kind ValidationError, however "valueTooLarge" of kind ValidationError was thrown as an alternative
Values lower than 0 ought to throw an error

Discover how the message explains precisely which error we obtained (valueTooLarge) and the error that we anticipated (valueTooSmall). It is fairly handy that the #anticipate macro will truly inform us what we obtained and what we anticipated, making it straightforward to determine what might have gone unsuitable.

Including slightly remark identical to we did with the Boolean model makes it simpler to motive 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 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 in regards to the precise error being thrown, however simply that an error of a selected kind was thrown. For instance, I won’t care that my “worth too small” or “worth too massive” error was thrown, however I do care that the kind of error that acquired thrown was a validation error. I can write my take a look at like this to examine for that.

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

As a substitute of specifying the precise case on validation error that I anticipate to be thrown, I merely go ValidationError.self. It will enable my take a look at to go when any validation error is thrown. If for no matter motive I throw a unique sort of error, the take a look at would fail.

There is a third model of anticipate in relation to errors that we might use. This one would first enable us to specify a remark like we are able to in any anticipate. We are able to then go a closure that we wish to execute (e.g. calling attempt checkInput) and a second closure that receives no matter error we obtained. We are able to 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 wish to examine the related worth as effectively. This is what that might appear like.

@Check 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 fundamental, however we might develop this in the true world. That is actually helpful when you will have 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 normally I’ve fairly easy error checking, so I’m typically utilizing the very first model of anticipate that you simply noticed on this part. However I’ve positively dropped all the way down to this one after I needed to examine extra difficult circumstances to find out whether or not or not I acquired what I anticipated from my error.

What you want is, in fact, going to rely by yourself particular state of affairs, however know that there are three variations of anticipate that you should utilize when checking for errors, and that all of them have form of their very own downsides that you simply would possibly wish to keep in mind.

In Abstract

Normally, I consider testing libraries by how highly effective or expressive their assertion APIs are. Swift Testing has carried out a extremely good job of offering us with a reasonably fundamental however highly effective sufficient API within the #anticipate macro. There’s additionally the #require macro that we’ll discuss extra in a separate submit, however the #anticipate macro by itself is already an effective way to begin writing unit assessments. It offers loads 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 on your testing wants.

Be sure to take a look at this class of Swift testing on my web site as a result of I had loads 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, ensure you discover me on social media, I’d love to listen to from you.

Leave a Reply

Your email address will not be published. Required fields are marked *