初学者向けです。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, 2assert_in_delta/4assert_raise/2, 3assert_receive/3assert_received/2refute/1, 2refute_in_delta/4refute_receive/3refute_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, 2setup/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