Overview

Let your Agent calls go on a tangent.

Tangent provides functions and macros for bridging global Agent processes with ExUnit tests configured to be async: true.

Installation

def deps do
  [
    {:tangent, "~> 0.1.0"}
  ]
end

Problem Statement

When using Agent processes to store state, it is common practices to start the processes from an application's supervisor. Unit tests of the agent may use ExUnit.Callbacks.start_supervised/2 to test interactions in isolation, but when testing things like Phoenix controllers of Phoenix.LiveView, cross-process interactions may mutate the state of the global agent, causing test pollution across asynchronous processes.

Usage

Tangent is intended to be a drop-in replacement for the Agent module that ships with Elixir.

defmodule MyAgent do
  use Agent

  def start_link(_), do: Agent.start_link(fn -> 0 end, name: __MODULE__)
  def current(), do: Agent.get(__MODULE__, & &1)
  def increment(), do: Agent.get_and_update(__MODULE__, fn current -> {current + 1, current + 1} end)
  def decrement(), do: Agent.get_and_update(__MODULE__, fn current -> {current - 1, current - 1} end)
end

defmodule MyTangent do
  use Tangent

  def start_link(_), do: Tangent.start_link(fn -> 0 end, name: __MODULE__)
  def current(), do: Tangent.get(__MODULE__, & &1)
  def increment(), do: Tangent.get_and_update(__MODULE__, fn current -> {current + 1, current + 1} end)
  def decrement(), do: Tangent.get_and_update(__MODULE__, fn current -> {current - 1, current - 1} end)
end

When code is compiled in Mix.env() == :test, the underlying agent is swapped out for an interceptor. This interceptor keeps data in a global dataset, so by default it will act like a global Agent. Test processes can use Tangent.Test to register themselves as the parent of overloaded datasets. After overloading an agent, any child process of the test will access the overloaded, rather than the global, dataset.

defmodule MyTangentTest do
  use ExUnit.Case, async: true
  use Tangent.Test

  setup do
    Tangent.Test.overload(MyTangent)
  end

  describe "increment" do
    test "increments the saved value" do
      assert MyTangent.current() == 0
      assert MyTangent.increment() == 1
      assert MyTangent.current() == 1
    end
  end
end

defmodule MyOtherTangentTest do
  use ExUnit.Case, async: true
  use Tangent.Test

  setup do
    Tangent.Test.overload(MyTangent)
  end

  describe "decrement" do
    test "decrements the saved value" do
      assert MyTangent.current() == 0
      assert MyTangent.decrement() == -1
      assert MyTangent.current() == -1
    end
  end
end