Help us understand the problem. What is going on with this article?

ExUnit入門

More than 3 years have passed since last update.

初学者向けです。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

追記

watchするならこれがおすすめ。 https://github.com/lpil/mix-test.watch

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away