UPDATE: ElixirForum でよりよい解法を教えてもらったので共有します。
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でコードからテストを生成したくなることがあった。
テストの名前は以下のように生成がうまくいく。
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
が得られる。もう少し育てて、
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 件目がエラーになった時点で処理がうちきられる。これはそういうものだが、今回の私のユースケースには合致しない。
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
を参照できてしまっているように、変数のスコープという観点では不要なところまで漏れてしまっている。
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 を使うと、テストケース毎のスコープで変数を渡せる。その場合は context.registered
から受けとる。前回と異なり follwoing test
のところに @pair
の値が渡っていないことがわかるだろう。
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 があるようだ。
ExUnit.Case.register_test/4 は、test
の実装内部でも利用している関数だ。これを利用すると、いい感じにテストを組み立てることができそうだったが、私はライブラリを作りたいわけではないので、今回は利用しなかった。