Calling a function on map , struct or module

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…

Apply works like this:

:erlang.apply(Module, :function, [argument1, argument2, argument3])

But there is a better alternative than using apply in many cases:
Rather than using a keyword list for the mock, just define a module MyFakeModule:

defmodule MyFakeModule do
  def foo() do
    "very fake!"
  end
end

And test it like this:

test "mock call" do
    assert call_foo(MyFakeModule) == "very fake!"
end

This is exactly the kind of dispatch that happens inside of Protocols (based on the module of the struct name.) and more generally in Behaviours.

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

You’d need to purge the modules after the test, for example: https://github.com/elixir-lang/elixir/blob/master/lib/elixir/test/elixir/protocol_test.exs#L333-L342

Wouldn’t that only be the case in the example you showed, where you’re defining an implementation of a protocol that could leak outside the test?

No, you can use that whenever you define dynamically a module inside the test and don’t want the module to leak.

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.

Thanks !