Agents in Elixir

In a recent project on building a test suite similar to ExUnit in Elixir, I came across the issue of having to track or maintain state for keeping count of the number of assertions passing or failing when a tets suite is run.

I thought this would be the perfect opportunity to try out Agent. From the documentation, an Agent is an abstraction around state and allows different processes to share or store the state.

Since the test assertions are defined using a macro and runs in a separate module and process, using an agent would allow the separation of concerns by allowing the reporting code to reside in a separate module and triggered when needed. The agent can then be started when the actual assertion runs and accumulates a report at the end of the testing process.

The following snippet of code is my implementation of the above:

defmodule Assertion.Stats do
  @name __MODULE__

  def start_link do
    Agent.start_link(fn -> %{total: 0, passes: 0, failures: 0} end, name: @name)
  end

  def test_pass do
    Agent.update(@name, fn dict ->
      Dict.update(dict, :passes, &(&1), fn(val) -> val+1 end)
    end)

    update_test_case_count
  end

  def test_fail do
    Agent.update(@name, fn dict ->
      Dict.update(dict, :failures, &(&1), fn(val) -> val+1 end)
    end)

    update_test_case_count
  end

  def update_test_case_count do
    Agent.update(@name, fn dict ->
      Dict.update(dict, :total, &(&1), fn(val) -> val+1 end)
    end)
  end

  def count_for(term) do
    Agent.get(@name, fn dict -> Dict.get(dict,term) end)
  end

  def terms do
    Agent.get(@name, fn dict -> Dict.keys(dict) end)
  end

  # returns a new map with values reset
  def reset do
    Agent.update(@name, fn dict ->
      %{dict| passes: 0, failures: 0, total: 0}
    end)
  end

  def report do
    IO.puts """
    ===============================================
    TEST REPORT
    ===============================================
    Pass: #{Assertion.Stats.count_for(:passes)}
    Failures: #{Assertion.Stats.count_for(:failures)}
    Total test cases: #{Assertion.Stats.count_for(:total)}
    """

    Assertion.Stats.reset
  end
end

Within Agent.start_link, we define an initial map with the keys “passes”, “failures” and “totals” to keep track of the various states of the test cases. When a test passes or fails, we call ‘Assertion.Stats.test_pass’ or ‘Assertion.Stats.test_fail’ to update the map appropriately. Then, we call ‘update_test_case_count’ to update the total count for all the test cases run. Finally, we can call ‘report’ to generate a summary of the tests and ‘reset’ simply returns a new map with the values set to 0.

Within the actual macro that defines the assertion, we can invoke the reporter like so:

defmacro assert(expr, opts) do
# code to define the assertion
.....
#starts the agent
Assertion.Stats.start_link

if test_pass do
  Assertion.Stats.test_pass
else
  Assertion.Stats.test_fail
end
end

# finally after all the asertions are run
Assertion.Stats.report

The actual implementation can be found here