ExUnit.LetLazy

For those coming from RSpec and missing let.

Examples

Order does not matter

In the following example, note the way it’s not important what order you call let in. The value you provide is a macro expression evaluated when it is used via get.

defmodule AddTest do
  use ExUnit.Case, async: true
  use ExUnit.LetLazy

  # code under test
  defp add(a, b) do
    a + b
  end

  describe "add/2 with a = b, b = 15" do
    # order is not important, thanks Elixir macros
    let :a, get(:b)
    let :b, 15

    test "gives the correct answer" do
      assert add(get(:a), get(:b)) == 30
    end
  end
end

This is implemented using ETS.

Each test is a mashup of bits of context

Like in RSpec, you can use let to keep the LOC in your test files manageable. Consider this example. Instead of repeating the setup each time with slight variations, all the possible options for arguments are placed at the top, and inside each describe block, we pull in the combination that we want to test.

What’s more, we can re-use the exact same context easily in order to test different qualities of our function - e.g. one test to make assertions about the messages it sends, a separate test to make assertions about its return value.

defmodule ReuseTest do
  use ExUnit.Case, async: true
  use ExUnit.LetLazy

  let :a_case_1, %{large_data_structure_1a: "x"}
  let :a_case_2, %{large_data_structure_2a: "x"}
  let :b_case_1, %{large_data_structure_1b: "x"}
  let :b_case_2, %{large_data_structure_2b: "x"}
  let :call, my_function(get(:a), get(:b))

  # code under test

  defp my_function(a, a) do
    send self(), {:log, :error, :same}
    a
  end
  defp my_function(a, b) do
    Map.merge(a, b)
  end


  describe "my_function, a = b" do
    let :a, get(:a_case_1)
    let :b, get(:a)

    test "gives the correct answer" do
      assert get(:call) == %{large_data_structure_1a: "x"}
    end

    test "sends the right message" do
      get(:call)
      assert_received {:log, :error, :same}
    end
  end

  describe "my_function, a != b" do
    let :a, get(:a_case_1)
    let :b, get(:b_case_1)

    test "does not send the message" do
      get(:call)
      refute_received {:log, :error, _}
    end
  end

  describe "my_function, a case 1, b case 1" do
    let :a, get(:a_case_1)
    let :b, get(:b_case_1)

    test "gives the correct answer" do
      assert get(:call) == %{large_data_structure_1a: "x", large_data_structure_1b: "x"}
    end
  end

  describe "my_function, a case 1, b case 2" do
    let :a, get(:a_case_1)
    let :b, get(:b_case_2)

    test "gives the correct answer" do
      assert get(:call) == %{large_data_structure_1a: "x", large_data_structure_2b: "x"}
    end
  end
  
  # ... other combinations of a, b, and assertion ...
end

Installation

Add ex_unit_let_lazy to your list of dependencies in mix.exs:

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

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

See also

See also ExUnit.Let which is similar, but simpler, uses the test context structure, and thus relies on the ordering of let calls.