Home > Software design >  How to mock module attributes and other modules in Elixir?
How to mock module attributes and other modules in Elixir?

Time:07-01

I'm very new to elixir so this is probably basic but couldn't see much online

if I have the following code

defmodule A do
  def my_first_function do
    # does stuff
  end
end

defmodule B do
    @my_module_attribute A.my_first_function()
end

then how do I mock module A in my tests so I can just tell the tests what I want it to return?

also is it possible to just mock @my_module_attribute instead? would be good to have an answer to both approaches (and then what is considered the better pattern)

CodePudding user response:

I think perhaps you are wanting to use module attributes for more than what they are useful for, and perhaps there is some confusion over the exact definition of "mock".

Avoid thinking of module attributes as "class variables" -- even though they look like they might serve the same purpose and they are in the same place, their behavior is different. Be especially careful when a module attribute relies on a function call. One common pitfall is to use module attributes to store values read from configuration, e.g. using Application.fetch_env!/2. The problem is that the module attributes are evaluated at compile time (not at run time), so you can easily end up with an unexpected value. (The compiler now warns about this gotcha explicitly and there is now the Application.compile_env!/2 provided to better communicate that particular use case).

I usually reserve module attributes for raising the visibility of simple constants and I tend to avoid using them for storing the result of any function execution.

When it comes back down to "mocking", you still have to think about the fact that Elixir is compiled -- it's not Javascript. Someone geekier than me can explain the mechanics, but the module attributes don't exist the same way at runtime as they do at compile time.

"Mocking" during tests usually means substituting one module or function for another, and this swap is often easier to do at runtime. One common pattern looks a bit like Dependency Injection (but purists may object to the comparison).

Consider a function like this that relies on some OtherModule to do its work:

def my_function(input, opts \\ []) do
  other_module = Keyword.get(opts, :service, OtherModule)
  other_module.get_thing(input)
  # do more stuff...
end

Instead of hard-coding the OtherModule inside of my_function, its name is read out of the optional opts. This provides a useful way to override the output of that OtherModule when you need to test it.

In your test, you can do something like this:

test "example mock" do
      assert something = MyApp.MyMod.my_function("foo", service: MockService)
end

When the test provides MockService as the :service module, the get_thing function will be called on the MockService module and not on the OtherModule. If the provided module does not define a get_thing function, the code will fail to execute. This is where having a behaviour comes in handy because it helps guarantee that your module has implemented the needed functions. The Mox testing library, for example, relies on the behaviour contracts, but from the example above you can see premise.

If you squint, you can see that this "injection" approach is somewhat similar to how Javascript often accepts a callback function as an argument, but in Elixir it is more common to pass around module names instead of captured functions.

  • Related