3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-12-21

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 の実装内部でも利用している関数だ。これを利用すると、いい感じにテストを組み立てることができそうだったが、私はライブラリを作りたいわけではないので、今回は利用しなかった。

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?