ExUnitでテストをコードから生成する

UPDATE: [ElixirForum](https://elixirforum.com/t/parameterized-testing-with-exunit/18935/2?u=niku) でよりよい解法を教えてもらったので共有します。

```elixir
ExUnit.start()

defmodule ParameterizedTest do
  use ExUnit.Case, async: true

  for {lhs, rhs} <- [{"one", 1}, {"two", 2}, {"three", 3}] do
    test "#{lhs} convert to #{rhs}" do
      assert unquote(lhs) === unquote(rhs)
    end
  end
end
```

```
  1) test one convert to 1 (ParameterizedTest)
     parameterized_test.exs:7
     Assertion with === failed
     code:  assert "one" === 1
     left:  "one"
     right: 1
     stacktrace:
       parameterized_test.exs:8: (test)



  2) test three convert to 3 (ParameterizedTest)
     parameterized_test.exs:7
     Assertion with === failed
     code:  assert "three" === 3
     left:  "three"
     right: 3
     stacktrace:
       parameterized_test.exs:8: (test)



  3) test two convert to 2 (ParameterizedTest)
     parameterized_test.exs:7
     Assertion with === failed
     code:  assert "two" === 2
     left:  "two"
     right: 2
     stacktrace:
       parameterized_test.exs:8: (test)



Finished in 0.06 seconds (0.06s on load, 0.00s on tests)
3 tests, 3 failures

Randomized with seed 42167
```

以下は UPDATE 前に書いていた記事。

----

ExUnitでコードからテストを生成したくなることがあった。
テストの名前は以下のように生成がうまくいく。

```elixir
ExUnit.start()

defmodule AssertionsTest do
  use ExUnit.Case, async: true

  for {k, v} <- %{one: 1, two: 2, three: 3} do
    test "\"#{k}\" means #{v}"
  end
end
```

実行すると

```
  1) test three means 3 (AssertionsTest)
     assertions_test.exs:7
     Not implemented

  2) test two means 2 (AssertionsTest)
     assertions_test.exs:7
     Not implemented

  3) test one means 1 (AssertionsTest)
     assertions_test.exs:7
     Not implemented

Finished in 0.03 seconds (0.03s on load, 0.00s on tests)
3 tests, 3 failures
```

が得られる。もう少し育てて、

```elixir
ExUnit.start()

defmodule AssertionsTest do
  use ExUnit.Case, async: true

  for {k, v} <- %{one: 1, two: 2, three: 3} do
    test "#{k} means #{v}" do
      assert k == v
    end
  end
end
```

として実行すると、以下のようにうまくいかない。
`test` マクロの `do` ブロックの中はスコープが異なるように設計されているためだ。そのおかげで外部からおかしなものを持ち込まない環境でテストできるようになっている。とはいえ、私がやりたいようなケースでは不便だ。

```
** (CompileError) assertions_test.exs:8: undefined function k/0
    (stdlib) lists.erl:1338: :lists.foreach/2
```

方向をかえて `test` の中で `assert` ではだめなのか。これは期待通りに動くのだが、テストが成功したとき it works しか表示されず、どんなテストがあるのか見通しがよくないと考えている。
また、テストが失敗したときも `test` の中で 1 件目がエラーになった時点で処理がうちきられる。これはそういうものだが、今回の私のユースケースには合致しない。

```elixir
ExUnit.start()

defmodule AssertionsTest do
  use ExUnit.Case, async: true

  test "it works" do
    for {k, v} <- %{one: 1, two: 2, three: 3} do
      assert k == v
    end
  end
end
```

```
  1) test it works (AssertionsTest)
     assertions_test.exs:6
     Assertion with == failed
     code:  assert k == v
     left:  :one
     right: 1
     stacktrace:
       assertions_test.exs:8: anonymous fn/2 in AssertionsTest."test it works"/1
       (stdlib) maps.erl:257: :maps.fold_1/3
       assertions_test.exs:7: (test)

Finished in 0.03 seconds (0.03s on load, 0.00s on tests)
1 test, 1 failure
```

`@k k` や `@v v` のような形でモジュールアトリビュートに割り当てるとひとまず期待通りに動くようにできた。ただし、 `following test` のところでも `@k` や `@v` を参照できてしまっているように、変数のスコープという観点では不要なところまで漏れてしまっている。

```elixir
ExUnit.start()

defmodule AssertionsTest do
  use ExUnit.Case, async: true

  for {k, v} <- %{one: 1, two: 2, three: 3} do
    @k k
    @v v
    test "#{k} means #{v}" do
      assert @k == @v
    end
  end

  test "following test" do
    assert @k == @v
  end
end
```

```
  1) test one means 1 (AssertionsTest)
     assertions_test.exs:9
     Assertion with == failed
     code:  assert @k == @v
     left:  :one
     right: 1
     stacktrace:
       assertions_test.exs:10: (test)

  2) test two means 2 (AssertionsTest)
     assertions_test.exs:9
     Assertion with == failed
     code:  assert @k == @v
     left:  :two
     right: 2
     stacktrace:
       assertions_test.exs:10: (test)

  3) test following test (AssertionsTest)
     assertions_test.exs:14
     Assertion with == failed
     code:  assert @k == @v
     left:  :two
     right: 2
     stacktrace:
       assertions_test.exs:15: (test)

  4) test three means 3 (AssertionsTest)
     assertions_test.exs:9
     Assertion with == failed
     code:  assert @k == @v
     left:  :three
     right: 3
     stacktrace:
       assertions_test.exs:10: (test)

Finished in 0.04 seconds (0.04s on load, 0.00s on tests)
4 tests, 4 failures
```

[ExUnit.Case.register_attribute/3](https://hexdocs.pm/ex_unit/1.7/ExUnit.Case.html#register_attribute/3) を使うと、テストケース毎のスコープで変数を渡せる。その場合は `context.registered` から受けとる。前回と異なり `follwoing test` のところに `@pair`の値が渡っていないことがわかるだろう。

```elixir
ExUnit.start()

defmodule AssertionsTest do
  use ExUnit.Case, async: true

  ExUnit.Case.register_attribute __ENV__, :pair

  for {k, v} <- %{one: 1, two: 2, three: 3} do
    @pair {k, v}
    test "#{k} means #{v}", context do
      {k, v} = context.registered.pair
      assert k == v
    end
  end

  test "following test" do
    assert @pair
  end
end
```

```
  1) test three means 3 (AssertionsTest)
     assertions_test.exs:10
     Assertion with == failed
     code:  assert k == v
     left:  :three
     right: 3
     stacktrace:
       assertions_test.exs:12: (test)

  2) test following test (AssertionsTest)
     assertions_test.exs:16
     Expected truthy, got nil
     code: assert @pair
     stacktrace:
       assertions_test.exs:17: (test)

  3) test one means 1 (AssertionsTest)
     assertions_test.exs:10
     Assertion with == failed
     code:  assert k == v
     left:  :one
     right: 1
     stacktrace:
       assertions_test.exs:12: (test)

  4) test two means 2 (AssertionsTest)
     assertions_test.exs:10
     Assertion with == failed
     code:  assert k == v
     left:  :two
     right: 2
     stacktrace:
       assertions_test.exs:12: (test)

Finished in 0.05 seconds (0.05s on load, 0.00s on tests)
4 tests, 4 failures
```

# 他の解決方法のようなもの

こういったテストはパラメーターからテストを生成する方式なので parameterized testing と呼ぶ。ExUnit で parameterized testing するライブラリに [KazuCocoa/ex_parameterized](https://github.com/KazuCocoa/ex_parameterized) があるようだ。

[ExUnit.Case.register_test/4](https://hexdocs.pm/ex_unit/1.7/ExUnit.Case.html#register_test/4) は、`test` の実装内部でも利用している関数だ。これを利用すると、いい感じにテストを組み立てることができそうだったが、私はライブラリを作りたいわけではないので、今回は利用しなかった。