Extending Modules
Learn how to use macros to extend modules and build an Assertion framework.
A core purpose of macros is to inject code into modules to extend their behavior, define functions, and perform any other required code generation.
For our Assertion
framework, our goal is to extend other modules with a test macro. The macro will accept a test-case description as a string, followed by a block of code where assertions can be made. The description will prefix failure messages to help debug the failing test cases. We’ll also define the run/0
function automatically for the caller so that a single function call can execute all test cases.
Our goal throughout this section is to produce the following testing domain-specific language (DSL), which extends any module with our mini testing framework.
Take a look at this code, but don’t worry about keying it in just yet:
defmodule Assertion do defmacro __using__(_options) do quote do import unquote(__MODULE__) Module.register_attribute __MODULE__, :tests, accumulate: true @before_compile unquote(__MODULE__) end end defmacro __before_compile__(_env) do quote do def run, do: Assertion.Test.run(@tests, __MODULE__) end end defmacro test(description, do: test_block) do test_func = String.to_atom(description) quote do @tests {unquote(test_func), unquote(description)} def unquote(test_func)(), do: unquote(test_block) end end defmacro assert({operator, _, [lhs, rhs]}) do quote bind_quoted: [operator: operator, lhs: lhs, rhs: rhs] do Assertion.Test.assert(operator, lhs, rhs) end end end defmodule Assertion.Test do def run(tests, module) do Enum.each tests, fn {test_func, description} -> case apply(module, test_func, []) do :ok -> IO.write "." {:fail, reason} -> IO.puts """ =============================================== FAILURE: #{description} =============================================== #{reason} """ end end end def assert(:==, lhs, rhs) when lhs == rhs do :ok end def assert(:==, lhs, rhs) do {:fail, """ Expected: #{lhs} to be equal to: #{rhs} """ } end def assert(:>, lhs, rhs) when lhs > rhs do :ok end def assert(:>, lhs, rhs) do {:fail, """ Expected: #{lhs} to be greater than: #{rhs} """ } end end
If we test it ...