テストが好きでたまらない皆さん、こんにちわ。17日目ですね。昨日はk1completeさんによるアナフォリックマクロを作ってみるでした。
今回はElixirのテストライブラリShouldIを紹介します。ShouldIを使うと、ネストしたテストが書けます。
導入
mix.exsのdepsに以下を追加します。only: :testをお忘れなく。
{:shouldi, "~> 0.2", only: :test}
そんでmix deps.getしましょう。
使う
なんのモジュールか分かんないですが、これをテストすることにします。
defmodule Member do
defstruct [:name, :age]
def bob, do: %__MODULE__{name: "Bob", age: 22}
def mary, do: %__MODULE__{name: "Mary", age: 28}
end
Member.bobで返ってくるメンバーの名前が"Bob"であることと、MaryはBobより年上であることを確認します。
defmodule ExampleTest do
use ExUnit.Case
use ShouldI
with "bob" do
setup context do
[bob: Member.bob]
end
should "be Bob", context do
assert context.bob.name == "Bob"
end
with "mary" do
setup context do
[mary: Member.mary]
end
should "be older than bob", context do
assert context.mary.age > context.bob.age
end
end
end
end
まずはuse ExUnit.Caseとuse ShouldIします。将来use ShouldIだけでOKになるっぽいです。
withでネスト、shouldでテストケースを書きます。setupに与えたdoブロックの戻り値は、テストケース内でcontext経由で使えます(context.bobやcontext.mary)。上の階層のsetupが順に呼ばれ結果が全てマージされるので、最後のshouldではbobもmaryも参照できます。お察しの通り、内部ではExUnitのsetupが使われています。
実際にテストを走らせるには、今まで通り
$ mix test
です。test_helper.exsはイジらなくて大丈夫です。
setupの実行タイミング
先ほど、上の階層のsetupが順に呼ばれると書きましたので、どのように実行されるのか見てみます。例えばこんなの。
with "with 1" do
setup context do
IO.puts "setup 1"
[]
end
should "should 1" do
IO.puts "should 1"
end
with "with 2" do
setup context do
IO.puts "setup 2"
[]
end
should "should 2-1" do
IO.puts "should 2-1"
end
should "should 2-2" do
IO.puts "should 2-2"
end
end
end
テストを走らせると、こんな感じの出力がされます。
setup 1
setup 2
should 2-1
setup 1
should 1
setup 1
setup 2
should 2-2
各テストケースの実行順はランダムですが、
- 最上層の
setup - 次の層の
setup - ...
should
という順序は当然保証されます。setupは何度も呼ばれるので、副作用のあることをする場合は注意が必要です。
どうしても最初に一度だけ実行したい処理を書きたい場合は、ShouldIに拘らずにExUnitの素の機能を使えばできます。
setup_all do
{:ok, [one: 1]}
end
with "setup_all" do
should "have context.one", context do
assert context.one == 1
end
end
ただしこの場合、withの中にさらにsetup_allを入れるのはNGです。想定通りに動きません。ぜーんぶまとめて最初に実行されてしまいます。この挙動はExUnit単体で使ったときも同様です。
Matchers
ShouldIにはShouldI.Matchers.ContextとShouldI.Matchers.Plugというモジュールが用意されています。
Matchers.Context
ShouldI.Matchers.Contextモジュールにはsetupで作られてきたcontextをテストするマクロが提供されています。
| まっちゃ | 中身 |
|---|---|
should_assign_key(key: "value") |
assert context(:key) == "value" |
should_match_key(key: {:ok, _}) |
assert {:ok, _} = context(:key) |
should_have_key(:key) |
assert Dict.has_key?(context, :key) |
should_not_have_key(:key) |
refute Dict.has_key?(context, :key) |
ワタクシのやり方が悪いんだと思うんですけど、どんなテストも通ってしまうので、誰か正しい使い方と使い所を教えて下さい。
Matchers.Plug
ShouldI.Matchers.Plugモジュールはplugのテストに特化したマッチャーを提供してくれます。
context.connectionにテスト対象の%Plug.Conn{}があることが前提になっています。
| マッチャ | 中身 |
|---|---|
should_respond_with(:success) |
assert context.connection.status in 200..299 |
should_respond_with(:redirect) |
assert context.connection.status in 300.399 |
should_respond_with(:bad_request) |
assert context.connection.status == 400 |
should_respond_with(:unauthorized) |
assert context.connection.status == 401 |
should_respond_with(:missing) |
assert context.connection.status == 404 |
should_respond_with(:error) |
assert context.connection.status in 500..599 |
should_match_body_to("a-z+") |
assert context.connection.resp_body =~ ~r"a-z+" |
いやーなんかこれも、ワタクシのやり方が悪いんだと思うんですけど、**エラーメッセージが全部multiple matcher errors**になってしまい、テストがどう落ちてるのか分からないので、誰か正しい使い方教えてください。
defmatcher
マッチャーを自分で定義することもできるらしいのですが、ContextもPlugもイケてないので、自分で作ってもあんまりイケてる感じにはならないと思います。それよりTestHelperモジュールとか作ったほうがいいと思います。そのほうが読みやすいです。
注意: 日本語
withやshouldの引数に日本語は使えません。日本語に限らず、atomに使えない文字は全部エラーです。
with "ボブ" do
should "彼の名はBobである", context do
...
end
end
# => (ArgumentError) argument error
# :erlang.binary_to_atom("test with 'ボブ': should 彼の名はBobである", :utf8)
まとめ
- ElixirのテストライブラリShouldIを紹介しました。
-
withとshouldによるネスト構文とsetupだけを利用するのが(今のところ)ベストだと思います。 - マッチャーは今後の開発に期待です。
余談
hex.pmで"test"を検索するといくつか出てきますが、良さ気なのはShouldIとamritaです。
こんな記事書いておいてアレですが、amritaの方がよく出来ています。ただ、どちらもダウンロード数がパッとしないですし、githubを見てても、みんなExUnitで満足しているようです。phoenixのテストも全部ExUnitです。RspecよりTest::Unitみたいな動きがあるのか、言語の性質的にExUnitで十分なのか、どうなんでしょ。
明日、18日目は@keithseahusです。