Search code examples
elixirex-unit

Enum.each loses variable when looping over an ExUnit test


When running the following code I get the warning:

warning: variable "char" does not exist and is being expanded to "char()", please use parentheses to remove the ambiguity or change the variable name
  test/my_module_test.exs:7

Followed by a failed test:

== Compilation error in file test/my_module_test.exs ==
** (CompileError) test/my_module_test.exs:7: undefined function char/0
    (stdlib) lists.erl:1338: :lists.foreach/2
    (stdlib) erl_eval.erl:680: :erl_eval.do_apply/6
    (elixir) lib/code.ex:767: Code.require_file/2
    (elixir) lib/kernel/parallel_compiler.ex:209: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/6
defmodule MyModule do
  use ExUnit.Case, async: true
  doctest MyModule

  Enum.each ~w(a b c), fn char ->
    test "test involving #{char}" do
      assert char == char
    end
  end
end

It seems that within the Enum.each block the value of char is defined for the line test "... #{char}" do but becomes undefined within the assertions.

Why is this happening?


Solution

  • ExUnit.test/3 is a macro that defines a function for you.

    Every time you define a new function in Elixir, it starts a new scope. This means any variable defined outside of the function won't be available inside the function. For example, you can't do this:

    foo = 1
    def some_function() do
      foo
    end
    

    There are some ways you can bypass this mechanism. One is to use unquote to inject some values as AST. However, in this case, the simplest approach is to put the value in a module attribute, so you can read it inside the function:

    @foo 1
    def some_function() do
      @foo
    end
    

    Whether you want to run the same test with different inputs (although it likely means there are issues with the test structure design,) you might use ExUnit.Callbacks.setup_all/2 to setup tests context, or use module attributes as shown in the documentation for ExUnit.Case.test/3

    Enum.each ~w(a b c), fn char ->
      @char char
      test "test involving #{char}", ctx do
        assert @char == @char
      end
    end
    

    Module attributes are obviously visible from anywhere in a module after they are defined.