Elixir
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です。