SandboxRegistry

DialyzerCredo

We can use the sandbox registry to help create sandboxes for testing

Sandboxes help us test by allow us to specify a mock that will be utilzed only for the specific test, allowing us to modify the return value of a specific function only in test.

We can utilize this pattern by building around an adapter pattern, and using the sandbox in dev mode. Other ways to build this pattern include using a flag like sandbox? to enable sandbox mode and swap out calls to a sanbox

Example Sandbox

defmodule HTTPSandbox do
  @registry :http_sandbox
  @keys :unique

  def start_link do
    Registry.start_link(keys: @keys, name: @registry)
  end

  def set_get_responses(tuples) do
    tuples
    |> Map.new(fn {url, func} -> {{:get, url}, func} end)
    |> then(&SandboxRegistry.register(@registry, @state, &1, @keys))
    |> case do
      :ok -> :ok
      {:error, :registry_not_started} -> raise_not_started!()
    end

    # Random sleep is needed to allow registry time to insert
    Process.sleep(50)
  end


  def get_response(url, headers, options) do
    func = find!(:get, url)

    func.(url, headers, options)
  end

  def find!(method, url) do
    case SandboxRegistry.lookup(@registry, @state) do
      {:ok, funcs} ->
        find_response!(funcs, method, url)

      {:error, :pid_not_registered} ->
        raise """
        No functions registered for #{inspect(self())}
        Action: #{inspect(action)}
        URL: #{inspect(url)}

        ======= Use: =======
        #{format_example(action, url)}
        === in your test ===
        """

      {:error, :registry_not_started} ->
        raise """
        Registry not started for #{inspect(__MODULE__)}.
        Please add the line:

        #{inspect(__MODULE__)}.start_link()

        to test_helper.exs for the current app.
        """
    end
  end
end

Now we can use this in an HTTP module only in test by doing

defmodule HTTP do
  @defaults_opts [
    sandbox?: Mix.env() === :test
  ]

  def get(url, header, opts) do
    opts = Keyword.merge(@defaults_opts, opts)

    if opts[:sandbox?] do
      HTTPSandbox.get_response(url, headers, opts)
    else
      make_get_request()
    end
  end
end

Now in test we have the ability to mock out our get requests per test

describe "some get request" do
  test "test get /url" do
    HTTPSandbox.set_get_responses([{
      "myurl.com",
      fn _url, _headers, _opts -> {:ok, :whatever} end
    }])

    assert {:ok, :whatever} === HTTP.get("myurl.com", [], [])
  end
end

Installation

Available in Hex, the package can be installed by adding sandbox_registry to your list of dependencies in mix.exs:

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

The docs can be found at https://hexdocs.pm/sandbox_registry.