初学者向けです。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
小ネタ
lib
とtest
以下を監視して、変更があったらテストを走らせます。--stale
はお好みで。
fswatch lib test | mix test --listen-on-stdin --stale
追記
watchするならこれがおすすめ。 https://github.com/lpil/mix-test.watch