I’m trying to pass a “mock” version of a module, and calling a function of it.
Is there a way to use a keyword list or map to do this ?
Basically I would like this test to pass:
defmodule DynamicModuleTest do
use ExUnit.Case
defmodule Real do
def foo() do
"real"
end
end
def call_foo(mock \\ Real) do
# This does not work, I get
mock.foo()
end
test "real call" do
# This works
assert call_foo() == "real"
end
test "mock call with keyword list" do
# This does not work:
# ** (ArgumentError) argument error
# stacktrace:
# :erlang.apply([foo: #Function<0.130984717/0 in DynamicModuleTest.test mock call/1>], :foo, [])
mock = [
foo: fn () -> "mock" end
]
assert call_foo(mock) == "mock"
end
# Or either:
test "mock call with map" do
# This does not work
# ** (KeyError) key :foo not found in: %{"foo" => #Function<1.5302493/0 in DynamicModuleTest.test mock call with map/1>}
mock = %{
"foo" => fn () -> "mock" end
}
assert call_foo(mock) == "mock"
end
end
I tried using apply to call the function, but it does not accepts keyword list or maps either…
You’re right, I should have mentioned that passing a fake module seems to works fine (that’s the first test in my test case, and that’s what I do at the moment.)
The problem is that, when you want to write several tests that use different version of the module, I can do this:
test "Test with success case" do
defmodule Mock do
def foo do
"ok"
end
end
...
end
test "Test with error case" do
defmodule Mock do
def foo do
"error"
end
end
...
end
Then I get a warning because I’m redefining the Mock module (which makes sense.)
warning: redefining module Mock (current version defined in memory)
Should I just live with the warning ? Is there a way to disable it ? Or can I undefine the module ?
Indeed, redefining the same module is a very bad idea.
However, why do you try to do this?
If you pass the module name to your function, then there is no need for the module name to be the same.
Instead:
test "Test with success case" do
defmodule SuccessfullMock do
def foo do
"ok"
end
end
...
assert call_foo(SuccessfullMock) == "ok"
...
end
test "Test with error case" do
defmodule ErrorMock do
def foo do
"error"
end
end
...
assert call_foo(ErrorMock) == "error"
end
And if you want to re-use the same mock implementation (say, ErrorMock) for multiple tests, then you can just move the defmodule ErrorMock do ... end to outside of the test definition.
And if you want to allow changing what kind of behaviour module is used in a more general way where it does not need to be passed explicitly to call_foo each time, you can do something like this:
def call_foo() do
behaviour_module = Application.get_env(:your_library_name, :call_foo_behaviour_module, DefaultBehaviourModule)
call_foo(behaviour_module)
end
def call_foo(behaviour_module) do
behaviour_module.foo()
end
The problems I see here (although I can agree there are debatable):
picking a different name for the mock for each test is a bit cumbersome (and I’m used to having libraries generate mocks for me without having to think about it :D)
As I understand it, using Application.get_env, if I set the mock in a test, it will “leak” to other tests ? I’m concerned this could make some tests flaky (and I kinda prefer the expliciteness of passing the dependency function by function)
However, I agree that if a specific implementation of the Mock module can be reused (typically a Dummy version), it should be declared outside of tests.)
Now, I was wondering if there was some kind of metaprogramming trick that could make it possible to define modules “on the fly”, but there does not seem to.