• 20
    いいね
  • 0
    コメント

初学者向けです。ExUnitというテストフレームワークがあることは知っているけれど、使い方はあんまり分かっていない、という方を対象に書きます。

テストを書く

基本の流れ

# my_module_test.exs

ExUnit.start

defmodule MyApp.MyModuleTest do
  use ExUnit.Case, async: true

  test "the truth" do
    assert 1 + 1 == 2
  end
end

はじめにExUnit.start:ex_unitを起動します。通常のMixプロジェクトでは、プロジェクト作成時に生成されるtest/test_helper.exsでこれが行われるので、自分で書くことは少ないでしょう。

次にテストモジュールを定義します。モジュール名は重複しなければ何でもOKですが、ファイル名は_test.exsで終わる必要があるので、これに合わせて、<プロジェクト名>.<テストしたいモジュール名>Testとするのが一般的です。

テストモジュールは、まずuse ExUnit.Caseから始めます。この中で、いくつかのモジュール属性の設定やテストに使う関数のimportなどをしています。async: trueオプションは、他のテストモジュールと並行に走らせるかを指定しています。Mockを使う場合など、特定のケース以外はtrueでよいと思います。デフォルトはfalseです。

テストの記述にはtestマクロを使います。以下で説明するアサーション関数、マクロを使って、意図した挙動か確かめます。

アサーション

ExUnitでは以下のアサーションが用意されています。単にtrue, falseか見るものもあれば、特定のメッセージが送られてくるかを見るものもあります。

  • assert/1, 2
  • assert_in_delta/4
  • assert_raise/2, 3
  • assert_receive/3
  • assert_received/2
  • refute/1, 2
  • refute_in_delta/4
  • refute_receive/3
  • refute_received/2

詳細はExUnit.Assertionsのドキュメントをあたってもらうとして、最もよく使うのはassert/1マクロです。assert/1に任意の式を与えると、以下のようにパースしてエラーを表示してくれます。

assert 1 + 1 == 3

#  Assertion with == failed
#  code: 1 + 1 == 3
#  lhs:  2
#  rhs:  3


assert 1 + 1 > 3

#  Assertion with > failed
#  code: 1 + 1 > 3
#  lhs:  2
#  rhs:  3


assert 1 in [2, 3, 4]

#  Assertion with in failed
#  code: 1 in [2, 3, 4]
#  lhs:  1
#  rhs:  [2, 3, 4]


assert 1 + 1 != 2

#  Assertion with != failed
#  code: 1 + 1 != 2
#  lhs:  2
#  rhs:  2

コールバック

以下のコールバック関数を用いることで、テスト実行前に任意の処理を行うことができます。

  • setup_all/1, 2
  • setup/1, 2

また、それらの中でon_exit/1マクロを使うと、テスト実行後の処理を記述できます。

defmodule MyApp.MyModuleTest do
  use ExUnit.Case, async: true

  setup_all do
    IO.puts "setup_all"

    on_exit fn ->
      IO.puts "on_exit in setup_all"
    end
  end

  setup do
    IO.puts "setup"

    on_exit fn ->
      IO.puts "on_exit in setup"
    end
  end

  test "1" do
    # ...
  end

  test "2" do
    # ...
  end
end

以上のテストを実行すると、次の順に出力されます。

setup_all
setup
on_exit in setup
setup
on_exit in setup
on_exit in setup_all

コンテキスト

コールバックの戻り値として、{:ok, dict}、または単にdictを返すことで、それをテストの中で使うことができます。dictにはMap、またはKeyword Listを使います。setup_allで返したコンテキストはsetupの第1引数として受け取ることができ、setupの戻り値はsetup_allで返したコンテキストとマージされてtestの第2引数に渡ってきます。

defmodule MyApp.MyModuleTest do
  use ExUnit.Case, async: true

  setup_all do
    user = Fixture.insert(:user)

    %{user: user}
  end

  setup %{user: user} do
    article = Fixture.insert(:article, author: user)

    %{article: article}
  end

  test "user and article should ...", %{user: user, article: article} do
    # user と article を使ったテスト
  end
end

describe

describe/2を使うと、複数のテストをグループにまとめることができます。以下に挙げるようないくつかの制限がありますが、setupが有効な範囲を絞れたり、テストコードの見通しを良くするなどできます。

  • describeの中でdescribeは使えません
  • describeの中でsetup_allは使えません
defmodule MyApp.MyModuleTest do
  use ExUnit.Case, async: true

  setup_all do
    user = Fixture.insert(:user)

    %{user: user}
  end

  describe "user and article" do
    setup %{user: user} do
      article = Fixture.insert(:article, author: user)

      %{article: article}
    end

    test "should ...", %{user: user, article: article} do
      # user と article を使ったテスト
    end
  end
end

doctest

testディレクトリの中に書くテストの他に、ドキュメント内のコード片をテストすることができます。Elixirではmoduledoc属性やdoc属性を使って、それぞれモジュールや関数の使用例を記述することがあります。doctestを用いると、この使用例が実装と合っているかテストすることができます。

defmodule MyApp.MyModule do

  @doc """
  この関数は必ず1を返します

      iex> MyApp.MyModule.one
      1
  """
  def one, do: 1
end

上のようなモジュールとドキュメントがある場合、テストコードでdoctestにモジュール名を指定することで、ドキュメント内のコードをテストできます。

defmodule MyApp.MyModuleTest do
  use ExUnit.Case, async: true

  doctest MyApp.MyModule  # この場合、 assert MyApp.MyModule.one == 1 と等価

  test "..." do
    ...
  end
end

テストを実行する

基本のコマンド

mix test

mix testでテストが走ります。test/test_helper.exsを読んでから、test/**/*_test.exsにマッチするファイルを対象にテストが走ります。

特定のテストを実行する

ファイル指定

ファイル名や行番号をつけることで、特定のテストだけを実行できます。

# ファイルを指定して実行
mix test ./test/my_module_test.exs

# ファイルと行番号を指定して実行
mix test ./test/my_module_test.exs:12

--stale

--staleオプションを付けると、前回のテスト実行以降に変更のあったモジュールが影響するテストだけ実行することができます。

mix test --stale

タグ指定

@moduletag@tagを使うことで、テストにタグを付与することができます。テストにおいては、それを指定することで特定のテストを実行、またはスキップすることができます。

defmodule MyApp.MyModuleTest do
  use ExUnit.Case, async: true

  @tag :very_slow
  test "condition A" do
    ...
  end

  test "condition B" do
    ...
  end
end
# :very_slowタグが付いているテストだけ実行
mix test --only very_slow

# :very_slowタグが付いていないテストだけ実行
mix test --exclude very_slow

なお、:skipタグは特別な扱いで、これを付けた場合は--exclude無しに単にmix testとしてもスキップされます。

@tag :skip
test "always skipped" do
  ...
end

小ネタ

libtest以下を監視して、変更があったらテストを走らせます。--staleはお好みで。

fswatch lib test | mix test --listen-on-stdin --stale
この投稿は Elixir Advent Calendar 20162日目の記事です。