Checking Efx - testable effects for Elixir
Tags: elixir, testing, functional
Published: 2024-07-13 11:32:17

Mocking in Elixir is always a hot topic, mainly because of people coming from different other technologies having different expectation. The most "cannonical" solution, [Mox](https://hexdocs.pm/mox/Mox.html), gives a lot of security with regard to what the fake implementation returns, but requires a bit of a ceremony from the programmer. Other solutions like [Mock](https://github.com/jjh42/mock), [Rewire](https://github.com/stephanos/rewire) or [Bypass](https://hexdocs.pm/bypass/Bypass.html) offer more ad-hoc mocking, but t the price of the tests heving to be synchronous. Then there's [Mimic](https://github.com/edgurgel/mimic), which offers ad-hoc mocking and supports async
tests.

In this busy area, a new library appeared very recently. It's called [Efx](https://github.com/bravobike/efx) and its description does not even mention mocking:

> A library to declaratively write testable effects 

It immediately cought m attention, because it seems to implement what I described as "[purity injection](/2022/10/19/purity-injection-elixir/)" some time ago. As a programmer, you write a default impure implementation of the code, but you mark it as an "effect". Then in tests you replace a stateful implementation with something more predictable (you "inject purity") and you can enjoy nice, stable and
fast tests.

This post is a result of taking the library on a quick testing round. I decided to use it to the exact same problem as in the "purity injection" post. To remind, when you create a new order in your e-commerce application, you need to assign it a number. The number needs to be unique within a tenant and it should not contain ambiguous characters. The naive implementation, with which we started, looked like this:

```elixir
defmodule OrderNumber do
  def generate(tenant_id) do
    candidate = 
      :crypto.strong_rand_bytes(4) 
      |> Base.encode32(padding: false) 
      |> replace_ambiguous_characters()
    
    query = 
      from o in Order, 
      where: o.tenant_id == ^tenant_id and o.order_id == ^candidate
    
    if Repo.exists?(query), do: generate(tenant_id), else: candidate
  end
end
```

Now, let's change this code to support effects in Efx's terminology. We will have two: one for generating a random string and another to check if the sequence has already been used.

```elixir
defmodule Commerce.OrderNumber do
  use Efx

  def generate(tenant_id) do
    candidate =
      generate_random()
      |> replace_ambiguous_characters()

    case check_existence(candidate, tenant_id) do
      true -> generate(tenant_id)
      false -> candidate
    end
  end

  @spec generate_random :: String.t()
  defeffect generate_random() do
    :crypto.strong_rand_bytes(4)
    |> Base.encode32(padding: false)
  end

  @spec check_existence(String.t(), integer()) :: boolean()
  defeffect check_existence(candidate, tenant_id) do
    import Ecto.Query

    from(o in Commerce.Order,
      where: o.tenant_id == ^tenant_id and o.number == ^candidate)
    |> Commerce.Repo.exists?()
  end

  defp replace_ambiguous_characters(string) do
    string
    |> String.replace("I", "1")
    |> String.replace("O", "0")
  end
end
```

Both `generate_random/0` and `check_existence/2` effects are compiled to a regular functions, which are called inside `generate/1`. There's nothing too fancy in it, so let's see about testing it. In the first test we want to check if the ambiguous characters are replace, so we want our RNG to generate a sequence with such characters:

```elixir
defmodule Commerce.OrderNumberTest do
  use Commerce.DataCase, async: true
  use EfxCase

  alias Commerce.OrderNumber

  test "replace Os and Is" do
    bind(OrderNumber, :generate_random, fn -> "A5O0I1" end)
    bind(OrderNumber, :check_existence, fn _, _ -> false end)

    assert OrderNumber.generate(1) == "A50011"
  end
end
```

The `bind/3` function replaces the implementation of effects from a default one to a fake one. Now our `generate_random/0` function returns `A5O0I1` strings, which then replaces the ambiguous  characters. We also need to `bind` the implementation of `check_existence` here, even though we actually know that the database is empty so the check would pass. There are two reasons to do so:

1. We **want** to do it - isolate all side-effects in tests, so tests are faster and less reliant on the infrastructure
2. We **have** to do it - because Efx forces us to

If we don't `bind` the `check_existence`, we would get an error:

```
** (RuntimeError) Mock for function check_existence/2 missing
    (efx 0.1.11) lib/efx_case/mock.ex:95: EfxCase.Mock.get_fun/3
    (efx 0.1.11) lib/efx_case/mock_state.ex:66: anonymous fn/5 in EfxCase.MockState.call/4
    (elixir 1.16.0) lib/agent/server.ex:16: Agent.Server.handle_call/3
    (stdlib 5.2) gen_server.erl:1131: :gen_server.try_handle_call/4
    (stdlib 5.2) gen_server.erl:1160: :gen_server.handle_msg/6
    (stdlib 5.2) proc_lib.erl:241: :proc_lib.init_p_do_apply/3
Last message (from #PID<0.405.0>): {:get_and_update, #Function<1.67167648/1 in EfxCase.MockState.call/4>}
```

After this is done, we can test more elaborate cases:

```elixir
test "use first candidate when existence check is false" do
  bind(OrderNumber, :generate_random, fn -> "ABC" end)
  bind(OrderNumber, :generate_random, fn -> "123" end)
  bind(OrderNumber, :check_existence, fn _, _ -> false end)

  assert OrderNumber.generate(1) == "ABC"
end

test "use second candidate when first existence check is true" do
  bind(OrderNumber, :generate_random, fn -> "ABC" end, calls: 1)
  bind(OrderNumber, :check_existence, fn _, _ -> true end, calls: 1)
  bind(OrderNumber, :generate_random, fn -> "123" end)
  bind(OrderNumber, :check_existence, fn _, _ -> false end)

  assert OrderNumber.generate(1) == "123"
end
```

Here we `bind` the effects few times. The second test shows `calls: 1` option passed, which makes sure that `bind` only binds once. After that, the next bound implementation is used.

Finally, just to be sure, we may want to write a test actually calling the database:

```elixir
test "check against actual database" do
  bind(OrderNumber, :generate_random, fn -> "ABC" end, calls: 1)
  bind(OrderNumber, :check_existence, {:default, 2})
  bind(OrderNumber, :generate_random, fn -> "123" end)

  %Commerce.Order{tenant_id: 1, number: "ABC"}
  |> Commerce.Repo.insert!()

  assert OrderNumber.generate(1) == "123"
end
```

Binding with `{:default, arity}` lets us call the original implementation. This has to be done fully intentionally, not cutting corners.

We have successfully tested our order number generator in a much cleaner way, compared to my original post, without passing dependencies as an argument. Overall I think this is a very promising approach that, aside for improving using fake implementation in tests, nudges the developer in a nice way to:

* Think about side-effects when writing the code - you need to wrap them in `defeffect` 
* Keep the number of impure functions in the module as low as possible - you have to always bind them all 
* Use `@spec`s - `defeffect` requires them; although there are no type checks whether a fake implementation adheres
to the spec, at least having to define it might help to visually identify problems.

I think Efx is a great addition to Elixir ecosystem and I salute the author. Nice job!