How to use macros to fetch module attributes

Hello,
I am trying to reduce boilerplate stereotypical and rather brainless tests (that I still want to have in my library in case I make dumb mistakes down the road) that assert on fetching fields from a structure. The tests have this format:

test "batch_size" do
  assert C.get(@valid_opts, :batch_size) == @valid_batch_size
  assert C.get_batch_size(@valid_opts) == @valid_batch_size
end

The opportunity for code generation is there.

How can I, given a field name as an atom, generate the above code with macros (or even without)?

I am pretty bad at metaprogramming in Elixir still and will appreciate a pointer and an advice.

I wouldn’t use macros for the task as you have laid it out. I would do something like this:

@fields [
  {:batch_size, :get_batch_size, @valid_batch_size}
]

test "getting fields" do
  for {field, func_name, expected_result} <- @fields do
    assert C.get(@valid_opts, field) == expected
    assert apply(C, func_name, [@valid_opts]) == expected
  end
end

You could also generate one test per field, but personally I wouldn’t do that because it makes the code generation more complicated (I believe you’d need unquote) and they’re all effectively the same test anyway. Also instead of baking in @valid_batch_size into @valid_opts, I’d probably add a opts = Map.put(@valid_opts, expected) as the first line. That way if you want you could test different behavior depending on the value of :batch_size.

2 Likes

That’s a good way of doing it.

But assume for a minute that I’d like to only specify the field name as an atom and use meta-programming. How would you do it?

Module.get_attribute/2, so in this case it can be:

for {name, {opts, size}} <- [batch_size: {:valid_opts, :valid_batch_size}] do
  test name do
    assert C.get(unquote(Module.get_attribute(__MODULE__, opts)), unquote(name)) == unquote(Module.get_attribute(__MODULE__, size))
    assert apply(C, unquote(:"get_#{name}"), []) == unquote(Module.get_attribute(__MODULE__, size))
  end
end
2 Likes

Likewise to you: any way to do this by only specifying a single atom per tested field?

I tried several combinations but always ended up with undefined function x/0 since I tried to use a local variable in an unquote call, or getting messages that Module.get_attribute cannot be called in certain contexts.

This worked (EDIT: forgot to add full code first time around):

[:batch_size, :db_name, :exec_timeout, :genserver_timeout]
    |> Enum.each(fn(name) ->
      test name do
        expected = unquote(Module.get_attribute(__MODULE__, String.to_atom("valid_#{name}")))
        assert C.get(@valid_opts, unquote(name)) == expected
        assert apply(C, unquote(:"get_#{name}"), [@valid_opts]) == expected
      end
    end)

Thanks for the help, @axelson and @hauleth, you pointed me at the right direction.

1 Like