7 months ago
Exploring Mocking Solutions in Elixir: Introducing Modulex
I’ve recently delved into the world of mocking in Elixir and have been particularly intrigued by the Mox package, endorsed by José Valim. While studying this approach, I noticed that it could introduce a considerable amount of boilerplate code into a codebase, along with potential inconsistencies in how module references are managed in the application environment. I couldn’t help but think there had to be a more streamlined solution.
Take, for example, the typical module structure in such a setup:
defmodule MyModule do
@behaviour __MODULE__.Behaviour
def hello(name) do
application_env_module().hello(name)
end
def application_env_module() do
get_in(Application.get_env(:my_app, :modules), [:my_module]) || __MODULE__.Implementation
end
defmodule Implementation do
@behaviour MyModule.Behaviour
def hello(name) do
"Hello #{name}"
end
end
defmodule Behaviour do
@callback hello(name :: String.t()) :: any()
end
end
In this example, MyModule
serves as a facade that selects an appropriate module based on the application environment configuration. If a module atom is specified, it’s utilized; otherwise, the code defaults to the built-in implementation. However, this structure has some downsides:
- Boilerplate code that acts as a proxy to the underlying implementation could be automatically generated.
-
The method
application_env_module
and related naming conventions can become inconsistent across the codebase.
To tackle these challenges and experiment with Elixir macros, I created a new package for the Elixir ecosystem named modulex
. With this package, the previous example can be refactored as follows:
defmodule MyModule do
use Application.Module
defimplementation do
def hello(name) do
"Hello #{name}"
end
end
defbehaviour do
@callback hello(name :: String.t()) :: any()
end
end
Notice how much more concise and ergonomic the code has become. I chose to prioritize convention over configuration, thereby standardizing the naming of child modules and the keys within the application environment.
For those who use Mox or Hammox for mock definitions, you can easily set a mock like so:
# test_helper.exs
Mox.defmock(MyApplication.Module.mock_module(), for: MyApplication.Module.behaviour_module())
MyApplication.Module.put_application_env_module(MyApplication.Module.mock_module())
I’d love to hear any feedback on the implementation or the API design. This is my inaugural venture into Elixir macros, and the journey has been both rewarding and a process of trial and error.