Walkman - isolate your tests from the world

By: Derek Kraan / 2019-07-22

If you have seen Guardians of the Galaxy, you might remember the scenes where Chris Pratt has his Walkman on, shutting him off from the surrounding chaos. This is what Walkman does, for your test suite.

A new testing library

Elixir already has some good testing libraries, so let’s discuss a couple of them and figure out where Walkman fits in.

Mox - From Plataformatec, Mox is a library that allows you to define mocks based on behaviours. It supports concurrency, so asynchronous testing is still possible.

test "invokes add and mult" do
MyApp.CalcMock
|> expect(:add, fn x, y -> x + y end)
|> expect(:mult, fn x, y -> x * y end)

assert MyApp.CalcMock.add(2, 3) == 5
assert MyApp.CalcMock.mult(2, 3) == 6
end

Mock - This library allows you to mock out parts of the system ad-hoc. It applies mocks globally, so does not support asynchronous testing.

test "mock functions with multiple returns" do
with_mocks(HTTPotion, [
get: fn
"http://example.com" -> "<html>Hello from example.com</html>"
"http://example.org" -> "<html>example.org says hi</html>"
end
]) do
assert HTTPotion.get("http://example.com") == "<html>Hello from example.com</html>"
assert HTTPotion.get("http://example.org") == "<html>example.org says hi</html>"
end
end

Walkman - Inspired by Ruby’s VCR, Walkman wraps a module and records function calls to that module and can replay them back later.

test "MyModule" do
Walkman.use_tape "MyModule1" do
assert :ok = calls_my_module()
end
end

Technically, Walkman is a stubbing library, not a mocking library. Walkman records the input and output to a stubbed module to “tapes”, which are then replayed on subsequent test runs. This frees you from the drudgery of generating mock objects by generating stubs for you automatically, and it also reduces the coupling of your tests to your code, since you can simply generate new stubs when the code changes.

Dependency Injection

Walkman works best when you use dependency injection. Let’s say you have a module MyModule. You can generate a stub with a single line of code:

# test/test_helper.exs
Walkman.def_stub(MyModuleWrapper, for: MyModule)

Then you can configure your application to use MyModuleWrapper in the test environment:

# config/config.exs
config :my_app, my_module: MyModule

# config/test.exs
config :my_app, my_module: MyModuleWrapper

Now, instead of using MyModule directly, use Application.get_env(:my_app, :my_module) in your application code.

That’s it! Now you can write tests like this:

test "walkman test" do
Walkman.use_tape("walkman test") do
### your test here
end
end

The first time you run a test successfully, Walkman will record a test fixture. The next time you run that same test, the test fixture will be used, so your test will be isolated from whatever external service you were using.

Automatic re-recording

Walkman has some nice features to make testing as frictionless as possible. One of these is that Walkman will automatically re-record your tapes if a module used in one of those tapes changes. So if you have a test that calls into MyModuleWrapper and MyModule has changed, then Walkman will detect this and re-record the tape.

Concurrency

Walkman also supports asynchronous testing out of the box. By default, a tape will only be accessible from the process that called Walkman.use_tape/3. You can share the tape with other arbitrary processes by using Walkman.share_tape/2:

test "can share tape with another process" do
Walkman.use_tape "share_tape" do
test_pid = self()

spawn_link(fn ->
Walkman.share_tape(test_pid)

assert call_stub()
end)
end
end

Walkman.share_tape/2 accepts a pid as second argument, so you can share a tape with any other process. The second argument also has a default value of self(), so if you are running this from the process you would like to access the tape from, then you can leave it.

Global mode

Sometimes you might need to globally stub a module. This can’t be used with asyncronous tests. To do this you simply add global: true when calling Walkman.use_tape/3:

test "global tape" do
Walkman.use_tape "global_tape", global: true do
spawn_link(fn ->
assert call_stub()
end)
end
end

Integration mode

Walkman supports an integration mode that can be turned on by setting Walkman.set_mode(:integration) (in test/test_helper.exs for example). In this mode, no tapes will be used and all function calls will be forwarded to the actual module. I like to run these tests once a day to make sure changes in dependent services haven’t broken my application.

Conclusion

Walkman was inspired by Ruby’s VCR, but with the following important differences:

  1. VCR is for HTTP requests, but Walkman can be used for anything.
  2. There is no magic. You must explicitly use your stubbed module.

My hope is that Walkman finds a niche in Elixir’s constellation of testing libraries and that someone out there finds it useful!

Drop us a line

Get the ball rolling on your new project, fill out the form below and we'll be in touch quickly.

Recent Posts

Where to put startup code in Elixir

By: Derek Kraan / 2019-12-06

Walkman - isolate your tests from the world

By: Derek Kraan / 2019-07-22

Introducing MerkleMap: improving Horde's performance

By: Derek Kraan / 2019-05-20

What's new in Horde v0.5.0

By: Derek Kraan / 2019-05-06

Why should every process be supervised?

By: Derek Kraan / 2019-04-01

Implementing Connection Draining in Phoenix

By: Derek Kraan / 2019-01-24