Edited at
ElixirDay 2

ExUnit入門

More than 1 year has 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