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.