MecksUnit: Elegantly mock module functions in (async) ExUnit tests
It is a well-know topic within the Elixir community: “To mock or not to mock? :)”
Every alchemist probably has his / her own opinion concerning this topic. José Valim and Plataformatec has published the Hex package Mox which complies with his article on mocking in Elixir.
Personally, I’m not convinced in having to change the code “in service of” testing certain modules. Why would one add abstraction to code of which its purpose isn’t supposed to be interchangeable (with mock modules for instance)?
After some Googling, I found Espec of which I thought that that’s a little bit too much. Finally, I found Mock which could have done the job. But there are two downsides:
-
You cannot use
async: true
- Defining the mock functions could have been done in a more readable way
Based on that, I decided to write MecksUnit which solves just that. An example:
# (in test/test_helper.exs)
ExUnit.start()
MecksUnit.mock()
# (in test/mecks_unit_test.exs)
defmodule Foo do
def trim(string) do
String.trim(string)
end
end
defmodule MecksUnitTest do
use ExUnit.Case, async: true
use MecksUnit.Case
defmock String do
def trim(" Paul "), do: "Engel"
def trim(" Foo ", "!"), do: "Bar"
def trim(_, "!"), do: {:passthrough, [" Surprise! !!!!", "!"]}
def trim(_, _), do: :passthrough
end
defmock List do
def wrap(:foo), do: [1, 2, 3, 4]
end
mocked_test "using mocked module functions" do
task =
Task.async(fn ->
assert "Engel" == String.trim(" Paul ")
assert "Engel" == Foo.trim(" Paul ")
assert "Bar" == String.trim(" Foo ", "!")
assert " Surprise! " == String.trim(" Paul ", "!")
assert "MecksUnit" == String.trim(" MecksUnit ")
assert "Paul Engel" == String.trim(" Paul Engel ", " ")
assert [1, 2, 3, 4] == List.wrap(:foo)
assert [] == List.wrap(nil)
assert [:bar] == List.wrap(:bar)
assert [:foo, :bar] == List.wrap([:foo, :bar])
end)
Task.await(task)
end
test "using the original module functions" do
task =
Task.async(fn ->
assert "Paul" == String.trim(" Paul ")
assert "Paul" == Foo.trim(" Paul ")
assert " Foo " == String.trim(" Foo ", "!")
assert " Paul " == String.trim(" Paul ", "!")
assert "MecksUnit" == String.trim(" MecksUnit ")
assert "Paul Engel" == String.trim(" Paul Engel ", " ")
assert [:foo] == List.wrap(:foo)
assert [] == List.wrap(nil)
assert [:bar] == List.wrap(:bar)
assert [:foo, :bar] == List.wrap([:foo, :bar])
end)
Task.await(task)
end
end
Mocking module functions is pretty straightforward and done as follows:
-
Add
use MecksUnit.Case
at the beginning of your test file -
Use
defmock
as if you would define the original module withdefmodule
containing mocked functions -
Use
mocked_test
as if you would define a normal ExUnittest
after having defined all the required mock modules -
Add
MecksUnit.mock()
in yourtest/test_helper.exs
file
The defined mock modules only apply to the first mocked_test
encountered. So they are isolated (despite of :meck
having an unfortunate global effect) as MecksUnit takes care of it. Also, non-matching function heads within the mock module will result in invoking the original module function as well. And last but not least: you can just run the tests asynchronously.
Enjoy using MecksUnit (if you prefer unobtrusive mocking). A Github star is very welcome, haha ;)