Posted at
ElixirDay 17

テストライブラリ ShouldI

More than 3 years have passed since last update.

テストが好きでたまらない皆さん、こんにちわ。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.Caseuse ShouldIします。将来use ShouldIだけでOKになるっぽいです

withでネスト、shouldでテストケースを書きます。setupに与えたdoブロックの戻り値は、テストケース内でcontext経由で使えます(context.bobcontext.mary)。上の階層のsetupが順に呼ばれ結果が全てマージされるので、最後のshouldではbobmaryも参照できます。お察しの通り、内部では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

各テストケースの実行順はランダムですが、


  1. 最上層のsetup

  2. 次の層のsetup

  3. ...

  4. 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.ContextShouldI.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

マッチャーを自分で定義することもできるらしいのですが、ContextPlugもイケてないので、自分で作ってもあんまりイケてる感じにはならないと思います。それよりTestHelperモジュールとか作ったほうがいいと思います。そのほうが読みやすいです。


注意: 日本語

withshouldの引数に日本語は使えません。日本語に限らず、atomに使えない文字は全部エラーです。

with "ボブ" do

should "彼の名はBobである", context do
...
end
end
#=> (ArgumentError) argument error
# :erlang.binary_to_atom("test with 'ボブ': should 彼の名はBobである", :utf8)


まとめ


  • ElixirのテストライブラリShouldIを紹介しました。


  • withshouldによるネスト構文とsetupだけを利用するのが(今のところ)ベストだと思います。

  • マッチャーは今後の開発に期待です。


余談

hex.pmで"test"を検索するといくつか出てきますが、良さ気なのはShouldIamritaです。

こんな記事書いておいてアレですが、amritaの方がよく出来ています。ただ、どちらもダウンロード数がパッとしないですし、githubを見てても、みんなExUnitで満足しているようです。phoenixのテストも全部ExUnitです。RspecよりTest::Unitみたいな動きがあるのか、言語の性質的にExUnitで十分なのか、どうなんでしょ。

明日、18日目は@keithseahusです。