Generate tests through module attributes but local variable can't be found

Hey everyone,

For an upcoming ElixirForum post (and a small algorithm experimentation library) I am trying to generate unit tests like below. PP is the name of the module being tested and I’d like to only test functions whose names start with impl_ and have arity 1. @inputs_to_expected_results is a map.

  @function_names PP.__info__(:functions)
                  |> Enum.filter(fn {n, i} ->
                    Atom.to_string(n) |> String.starts_with?("impl_") and i == 1
                  end)
                  |> Enum.map(&elem(&1, 0))

  @function_names
  |> Enum.each(fn name ->
    @inputs_to_expected_results
    |> Enum.each(fn {input, expected} ->
      # IO.puts("Testing #{inspect(input)}, expecting result: #{inspect(expected)}")
      test inspect(input) do
        assert expected == apply(PP, unquote(name), [unquote(input)])
      end
    end)
  end)

The error I get is that the expected variable does not exist. Tried to wrap it with unquote like the other two variables but then I get told that the value is an invalid quoted expression.

I am stuck. What am I doing wrong?

That seems to work for me on a freshly generated mix project.

Summary
defmodule PP do
  def impl_abc(i) do
    i + 1
  end
end

defmodule MetaTestsTest do
  use ExUnit.Case
  doctest MetaTests

  @inputs_to_expected_results %{1 => 2}

  PP.__info__(:functions)
  |> Enum.filter(fn {n, i} -> Atom.to_string(n) |> String.starts_with?("impl_") and i == 1 end)
  |> Enum.each(fn {name, _arity} ->
    @inputs_to_expected_results
    |> Enum.each(fn {input, expected} ->
      # IO.puts("Testing #{inspect(input)}, expecting result: #{inspect(expected)}")
      test inspect(input) do
        assert unquote(expected) == apply(PP, unquote(name), [unquote(input)])
      end
    end)
  end)
end

I guess the devil is always in the details:

** (CompileError) test/pp_test.exs: invalid quoted expression: {[4, 7, 9, 65, 100], [1, 3, 5, 8, 11, 74, 83], 185, 185}

This is not garbled output. That’s the exact tuple I am expecting (two lists and two integers).

Can you slightly modify your fresh project with something that returns tuples?

Seems like your expected value is somehow messing up the AST. Also quickly made the thing a bit more readable.

defmodule PP do
  def impl_abc(i) do
    {[4, 7, 9, 65, 100], [i, 3, 5, 8, 11, 74, 83], 185, 185}
  end
end

defmodule MetaTestsTest do
  use ExUnit.Case
  doctest MetaTests

  @inputs_to_expected_results %{
    1 => {[4, 7, 9, 65, 100], [1, 3, 5, 8, 11, 74, 83], 185, 185},
    2 => {[4, 7, 9, 65, 100], [2, 3, 5, 8, 11, 74, 83], 185, 185}
  }
  @impl_funs Enum.filter(PP.__info__(:functions), fn {n, i} ->
               String.starts_with?("#{n}", "impl_") and i == 1
             end)

  for {name, _} <- @impl_funs,
      {input, expected} <- @inputs_to_expected_results,
      expected = Macro.escape(expected) do
    test inspect(input) do
      assert unquote(expected) == PP.unquote(name)(unquote(input))
    end
  end
end
1 Like

I was also reworking mine to use a for comprehension but yours was a little clearer than mine.

Macro.escape is what I was missing. My macro-fu is rusty, guess it’s time to pick up the “Metaprogramming Elixir” book which I have but never touched. :003:

Thanks a lot for the assist! :heart:

Just in case anyone comes across the question. We can also pass variables to the test context via @tag and no need for unquote and Macto.escape

  for {input, expected} <- @pairs do
    @tag input: input, expected: expected
    test "when inspect(input)", %{input: input, expected: expected} do
      assert process(input) == expected
    end
  end

In the case from the subject when even function name is variable we could use apply/3

apply(PP, name, [input])
1 Like