1
votes

I am using elixir and following the advice from the http://blog.plataformatec.com.br/2015/10/mocks-and-explicit-contracts/ blog post.

I have run into a problem of tracking which mock function corresponds to which test. I added a mock module for the test environment for an api wrapper. As I add mock functions to the mock api module, I have found that I dont remember which functions were written to return results for which tests.

I have been trying to figure out a way to use a macro to define the mocked method near the test. I am also interested in this question as a learning exercise.

Below is how I would envision it to work:

defmodule SomeMockModule do
end

defmodule MockUtil do
  defmacro add_mock module, block do
    # <THE MISSING PIECE>
  end
end

defmodule Test do
  use ExUnit.Case
  require MockUtil

  MockUtil.add_mock SomeMockModule do
    def some_func do
      "mock value"
    end
  end

  test "The mock value is returned" do
    assert SomeMockModule.some_func == "mock value"
  end
end

This is similar to the question about open modules: Open modules in Elixir? however I am wondering about how to do it at compile time not runtime.

I have looked around and I have not found anything that says it can or cannot be done at compile time.

In a way this is a fancy copy and paste :)

What I have tried so far:

1) The following works but seems rather messy. and it requires changes to the mock module. I am trying to figure out if there is a way to do it without the before compile.

defmodule MockUtil do
  defmacro register_function( _module, do: block )do
    Module.put_attribute Test, :func_attr, block
  end
end

defmodule Test do
  require MockUtil
  Module.register_attribute __MODULE__,
      :func_attr,
      accumulate: true, persist: false

  defmacro define_functions(_env) do
    @func_attr
  end

  MockUtil.register_function SomeMockModule do
    def foo_bar do
      IO.puts "Inside foo_bar."
    end
  end
end

defmodule SomeMockModule do
  @before_compile {Test, :define_functions}
end

SomeMockModule.foo_bar

2) I also tried, in the place of :

Module.eval_quoted module, block

however it throws an error:

could not call eval_quoted on module {:__aliases__, [counter: 0, line: 10], [:SomeMockModule]} because it was already compiled

I guess I am running into order of compile issues.

Is there a way to add a function to a module at compile time?

3

3 Answers

2
votes

Have you tried the use macro? You can read more about it here. If I understood your problem correctly, it seems like injecting functionality into your module with the &__using__/1 callback is what you need.

EDIT: I'm just not sure there is a way to add functions to a module at compile time without macros... We use the following macro:

defmacro define(name, value) do
  quote do
    def unquote(name), do: unquote(value)
  end
end

To define constants, maybe instead of value you could receive a block?

1
votes

I was able to figure out the following:

ExUnit.start

defmodule MockUtil do
  defmacro __using__(_opts) do
    quote do
      defmacro __using__(_env) do
        test_module = __MODULE__
        mock_module = __CALLER__.module
                      |> Atom.to_string
                      |> String.downcase
                      |> String.split(".")
                      |> tl
        name = "#{mock_module}_functions_attr" |> String.to_atom
        quote do
          unquote(test_module).unquote(name)()
        end
      end
    end
  end

  defmacro add_mock_function( module, do: block ) do
    mock_module = Macro.expand_once( module, __CALLER__)
                  |> Atom.to_string
                  |> String.downcase
                  |> String.split(".")
                  |> tl

    test_module = __CALLER__.module
    functions_attribute = "#{mock_module}_functions_attr" |> String.downcase |> String.to_atom

    first_time? = Module.get_attribute test_module, functions_attribute

    Module.register_attribute test_module,
        functions_attribute,
        accumulate: true, persist: false

    Module.put_attribute test_module, functions_attribute, block

    if first_time? == nil do
      ast = {:@, [], [{functions_attribute, [], test_module}]}
      name = "#{mock_module}_functions_attr" |> String.to_atom
      quote do
        defmacro unquote(name)(), do: unquote(ast)
      end
    end
  end
end

defmodule Test do
  use ExUnit.Case
  use MockUtil


  MockUtil.add_mock_function Mock do
    def foo do
      "Inside foo."
    end
  end

  test "Register function adds foo function" do
    assert  "Inside foo." == Mock.foo
  end

  MockUtil.add_mock_function Mock do
    def bar do
      "Inside bar."
    end
  end

  test "Register function adds bar function" do
    assert  "Inside bar." == Mock.bar
  end

  MockUtil.add_mock_function MockAgain do
    def baz do
      "Inside bar."
    end
  end

  test "Register function adds baz function" do
    assert  "Inside bar." == MockAgain.baz
  end
end

defmodule Mock do
  use Test
end

defmodule MockAgain do
  use Test
end

I was initially trying to avoid the calls to "use", but I need them so that the order of compilation is correct, and I think there no way to inject code into other modules anyway.

0
votes

Would you be able to provide a bit more information regarding "I have run into a problem of tracking which mock function corresponds to which test", because I think you may be overcomplicating this.

As per the article you linked, you'd use OTP application config to specify which module to use in which environment. In prod, you'd want to use the "real" HTTP client, for example.

# config/dev.exs
config :your_app, :module_to_mock, YourApp.Module.Sandbox

# config/test.exs
config :your_app, :module_to_mock, YourApp.Module.InMemory

# config/prod.exs
config :your_app, :module_to_mock, YourApp.Module.RealHTTP

Then, when you want to use the module, you'd simply grab it with

Application.get_env(:your_app, :module_to_mock)

In this example, the behaviour of the above modules would be along the lines of ...

  1. YourApp.Module.Sandbox - Hit's the development sandbox of whatever API you're interacting with, if there is one at all. It's not uncommon to simply just use YourApp.Module.InMemory during development, too. Just depends on what you're using it for.
  2. YourApp.Module.InMemory - All API interactions in this module would just return static, inline data. Such as a list of structs which represent what the real API would send back
  3. YourApp.Module.RealHTTP - Real HTTP interaction.

As the article also states, each of the above modules would implement the same behaviour (ie. Elixir behaviour via @behaviour, this ensures each module implements the necessary functions so you know that your InMemory module will work just as reliably as your RealHTTP module.

I realise I've pretty much just regurgitated a bit of the article, but beyond that, I'm not really understanding your problem.