When I write my Ruby tests, I’m a big fan of using test doubles and asserting those doubles receive messages with the correct arguments. I guess I like to practice what Justin Searls calls Discovery Testing.
One thing I’ve really enjoyed about Elixir is how easy it is to test, but I’ve also sort of missed the isolation that you get by using test doubles. Well, lucky for me, I’ve now seen what I think might be an even better version of that pattern, and it’s great for testing the edges of your application where you have to deal with the gnarly outside world (like IO).
I’ve been doing a big refactor on Benchee recently, and while working on that I ran across the following test:
test "asks to print what is currently benchmarking" do
test_suite()
|> benchmark("Something", fn -> :timer.sleep 10 end)
|> measure(TestPrinter)
assert_receive {:benchmarking, "Something"}
end
I thought “Woah, that looks just like an assertion on an object receiving a message in Ruby! What black magic is happening here?” Well, of course it wasn’t black magic, but the closest thing we have to it in Elixir - OTP!
So, the secret lies in that TestPrinter
module that we’re passing to the
measure/2
function. In the actual code we’re passing in a Printer
module
which has functions involved with printing stuff to the console. Of course we
want to ensure that this code is executed in our tests, but we don’t actually
want to print to the console the whole time. Also, exactly what is printed
isn’t really the scope of this test, so using capture_io
and doing some RegEx
magic on it to make sure stuff is printed would be duplicating test behavior.
So, instead we have this wonderful module:
defmodule Benchee.Test.FakeBenchmarkPrinter do
def duplicate_benchmark_warning(name) do
send self(), {:duplicate, name}
end
def configuration_information(_) do
send self(), :configuration_information
end
def benchmarking(name, _) do
send self(), {:benchmarking, name}
end
def fast_warning do
send self(), :fast_warning
end
def input_information(name, _config) do
send self(), {:input_information, name}
end
end
What that module does is replicate the same interface as the actual Printer
module, but instead of writing to the console, it sends messages to the current
process running that test. Then we can use the built in assert_receive
function in ExUnit to assert that the current process received a message
matching what we put in our FakeBenchmarkPrinter
module. I’ve seen a few other
examples of dependency injection in tests in Elixir that allow you to test a
given function in isolation - I’ve even written about a similar thing before -
but this was the first time I’d seen anything like this.
Pretty cool,huh? I can’t take credit for this, though - props to Tobias Pfeiffer for this one!