Julia Test Tutorial: Part 1 – How @test works

TDD works, at least a little bit. It's also very simple:

  1. Write tests the code will have to pass.
  2. Write the code.
  3. Run the tests.
  4. If the tests succeed, proceed.
  5. If not, debug and repeat. (Usually the code is what needs to be debugged; sometimes the tests need to be debugged; most rarely, but most valuably, you got both parts subtly wrong.)

The designers of Julia are seasoned devs. That's why they included a well-documented Test package right there in the standard library. But let's see if we can skip the documentation and just give you some tools right now to make your life easier.

@test is very simple: If you get back @test true when all is said and done, you get a Test Passed. If not, you get an error thrown your way.

julia> using Test

julia> # No need to add the Test package; it's in the standard library.

julia> @test 1 == 1
Test Passed

julia> @test 1 == 2
Test Failed at none:1
  Expression: 1 == 2
   Evaluated: 1 == 2
ERROR: There was an error during testing

Pretty straightforward. Let's try something a little spicier, like variable arguments and string concatenation.

julia> using Test

julia> f(x...) = x[1] * x[2]
f (generic function with 1 method)

julia> strings = ("Fire", "Water", "Air", "Earth")
("Fire", "Water", "Air", "Earth")

julia> @test f(strings) == strings[1] * strings[2]
Error During Test at none:1
  Test threw exception
  Expression: f(strings) == strings[1] * strings[2]
  BoundsError: attempt to access (("Fire", "Water", "Air", "Earth"),)
    at index [2]
  ###### (blah blah blah blah blah blah ..........) #######

ERROR: There was an error during testing

Oops. Looks like I did something wrong. But since I tested it now, I know that I did something wrong. If someone accidentally changes it down the line, and the @test fails again 6 months from now, we can trace it back quickly to this exact bit of code. So let's revise and try again.

What could've gone wrong? Ah. I see. We should have just passed our tuple elements as arguments, since f(x...) wraps them in a tuple anyway. Let's try again.

julia> using Test

julia> f(x...) = x[1] * x[2]
f (generic function with 1 method)

julia> @test f("Fire", "Water", "Air", "Earth") == "FireWater"
Test Passed

Beautiful. Now let's go back and figure out how we should actually put in a tuple, if we wanted to for a laugh. I seem to recall that ... works by “unfolding” tuples in an actual function call.

julia> strings = ("Mind", "Body", "Light", "Sound")

("Mind", "Body", "Light", "Sound")

julia> @test f(strings...) == strings[1] * strings[2]
Test Passed

And hey! What if we wanted to make sure the same error we made always gets thrown? After all, someone might change the code down the line in a subtle way that makes our ordinary @tests still pass, but breaks some try.... catch... finally code somewhere else. That could be a serious pain.

Introducing @test_throws. You'll notice that above, we had a BoundsError, because we tried to index into the 2nd place of a 1-tuple that contained the 4-tuple we actually cared above. So:

julia> using Test

julia> f(x...) = x[1] * x[2]
f (generic function with 1 method)

julia> @test_throws BoundsError f(("Mind", "Body", "Light", "Sound"))
Test Passed
      Thrown: BoundsError

Perfection.