...

/

Extending Modules

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 ...

Access this course and 1400+ top-rated courses and projects.