無印吉澤

ソフトウェア開発、運用管理(俗にいう DevOps)、クラウドコンピューティングなどについて、吉澤が調べたり試したことを書いていくブログです。

Elixir のテスティングフレームワーク ExUnit, ESpec の比較

f:id:muziyoshiz:20161122004038p:plain

Elixir でのコードを書くにあたり、テスティングフレームワーク ExUnit および ESpec を調べてみました。

ExUnit は Ruby で言うところの Test::Unit で、ESpec は RSpec のようです。調べてみた結果、自分は ExUnit を使うことにしたのですが、せっかくなので調べたことをまとめておきます。

比較表

ExUnit ESpec
インストール方法 Elixir, Phoenix Framework 標準 mix で追加する(espec, espec_phoenix)
テストの書き方 主に assert で真偽チェック expectshould で文章のように書ける
テストのグループ化 describe が使える(Elixir 1.3 以降) context, describe, example_group が使える
実行方法 mix test mix espec
テスト結果 失敗した行番号が含まれる 失敗した行番号が含まれない
テスト名の制約 日本語を使えない 日本語を使える

以下は、この比較表の詳細です。

ExUnit

インストール方法

Elixir の標準に含まれており、mix.exs に何も足さなくても使えます。

また、Phoenix Framework では mix phoenix.new を実行する際に、データベース接続のためのヘルパーや CaseTemplate が自動生成されます。これにより、ExUnit のテストケースを使ったモデルのテストが可能になっています。

テストの書き方

主に assert または refute での真偽チェックとして書く必要があります。

defmodule ElixirSampleTest do
  use ExUnit.Case
  doctest ElixirSample

  test "the truth" do
    assert 1 + 1 == 2
  end
end

標準出力をテストするための ExUnit.CaptureIO やログ出力をテストするための ExUnit.CaptureLog といった便利機能もあります。これらを使う場合も、最終的には assert を呼ぶ必要があります。以下は CaptureLog の使用例です。

  test "example" do
    assert capture_log(fn ->
      Logger.error "log msg"
    end) =~ "log msg"
  end

テストのグループ化

Elixir 1.3 から describe マクロが追加されて、テストのグループ化が簡単になりました。RSpec の describe と同じようにグループ化できます。

以下は ExUnit.Case のドキュメント に記載された例です。

defmodule StringTest do
  use ExUnit.Case, async: true

  describe "String.capitalize/1" do
    test "first grapheme is in uppercase" do
      assert String.capitalize("hello") == "Hello"
    end

    test "converts remaining graphemes to lowercase" do
      assert String.capitalize("HELLO") == "Hello"
    end
  end
end

実行方法

mix test で実行します。

$ mix test
..

Finished in 0.05 seconds
2 tests, 0 failures

Randomized with seed 37255

テスト結果

テストに失敗した場合、失敗したテストの開始行と、assertion に失敗した行の番号を表示します。

例えば、以下のように絶対失敗するテストを書いたとします。

defmodule ElixirSampleTest do
  use ExUnit.Case
  doctest ElixirSample

  test "the truth" do
    assert 1 + 1 == 2
    assert 1 + 1 != 2
  end
end

実行結果はこうなります。

$ mix test
.

  1) test the truth (ElixirSampleTest)
     test/elixir_sample_test.exs:5
     Assertion with != failed, both sides are exactly equal
     code: 1 + 1 != 2
     left: 2
     stacktrace:
       test/elixir_sample_test.exs:7: (test)



Finished in 0.04 seconds
2 tests, 1 failure

Randomized with seed 648187

テスト名の制約

test にも describe にも、日本語名(正確には、アトムに変換できない文字列)を使えないようです。例えば、以下のようにテストを書いたとします。

  test "1足す1が2に一致する" do
    assert 1 + 1 == 2
  end

これを実行すると、以下のようなエラーが返されます。

$ mix test
** (ArgumentError) argument error
    :erlang.binary_to_atom("test 1足す1が2に一致する", :utf8)
    (ex_unit) lib/ex_unit/case.ex:411: ExUnit.Case.register_test/4
    test/elixir_sample_test.exs:5: (module)
    (stdlib) erl_eval.erl:670: :erl_eval.do_apply/6
    (elixir) lib/code.ex:370: Code.require_file/2
    (elixir) lib/kernel/parallel_require.ex:57: anonymous fn/2 in Kernel.ParallelRequire.spawn_requires/5

BDD ではテストメソッド名に日本語を使おう、という意見があったりしますが、ExUnit では難しいようです。

ExUnit の参考ページ

Programming Phoenix: Productive, Reliable, Fast

Programming Phoenix: Productive, Reliable, Fast

ESpec

インストール方法

Elixir 標準ではないので、antonmi/espec のページにあるように mix.exs に追加する必要があります。

def deps do
  ...
  {:espec, "~> 1.3.0", only: :test},
  ...
end

あとはいつもの mix deps.get でインストールしたあとで、以下のコマンドを実行して spec/spec_helper.exs を生成すれば準備 OK です。

$ MIX_ENV=test mix espec.init

Phoenix Framework で ESpec を使う場合は、mix.exs に espec_phoenix を追加します。これにより、モデルによるデータベース接続を伴うテストなどが可能になります。

テストの書き方

expect … to の組み合わせか、should を使って書きます。RSpec だと今は should は推奨されていないようですが、ESpec についてはそういう記載は見当たりませんでした。

defmodule ContextSpec do
  use ESpec

  example_group do
    context "Some context" do
      it do: expect "abc" |> to(match ~r/b/)
    end

    describe "Some another context with opts", focus: true do
      it do: 5 |> should(be_between 4, 6)
    end
  end
end

テストのグループ化

context、describe、example_group が使えます。RSpec に似ていますね。

antonmi/espec に載っている例(下記)では example_group に説明文が付いていないですが、付けることも可能なようです。

defmodule ContextSpec do
  use ESpec

  example_group do
    context "Some context" do
      it do: expect "abc" |> to(match ~r/b/)
    end

    describe "Some another context with opts", focus: true do
      it do: 5 |> should(be_between 4, 6)
    end
  end
end

実行方法

MIX_ENV=test mix espec で実行します。ただ、antonmi/espec に従って mix.exs に設定を追加すれば、MIX_ENV=test は省略できるようになります。

$ mix espec
.

    1 examples, 0 failures

    Finished in 0.07 seconds (0.06s on load, 0.01s on specs)

    Randomized with seed 654062

テスト結果

テストに失敗した場合、失敗したテストの開始行を表示します。 assertion に失敗した行の番号は表示してくれません。

ExUnit の場合と同じように、絶対失敗するテストを書いてみます。

defmodule ElixirSampleSpec do
  use ESpec

  it "the truth" do
    expect(1 + 1) |> to(be 2)
    expect(1 + 1) |> to_not(be 2)
  end
end

すると、実行結果はこうなります。test の開始行の番号(4行目)しか出てきません。

$ mix espec
F

    1) ElixirSampleSpec the truth
    /Users/myoshiz/devel/elixir_sample/spec/elixir_sample_spec.exs:4
    Expected `2` not to equals (==) `2`, but it does.

    1 examples, 1 failures

    Finished in 0.11 seconds (0.1s on load, 0.01s on specs)

    Randomized with seed 728628

これくらいの内容なら、テスト結果からどの行かわかりますが、テストあたりの行数が長くなると辛くなってきます。長いテストコードは書くべきでない、という設計思想なんでしょうか。

コマンドライン引数などで、この動作を変更できるのではないかと思ったのですが、方法を見つけられませんでした……。これ、個人的には致命的に不便だと思うんですけど、他の人はどうなんですかね。

テスト名の制約

ESpec の it や describe は、テスト名に日本語を含めても、問題なく動作しました。

ESpec の参考ページ

まとめ

ESpec で書いたテストのほうが読みやすくなるのですが、総合的に考えて、個人的には今後は ExUnit を使うことにしました。

ExUnit は標準で採用されているために導入の手間が少なく、グループ化などの基本的な機能は備えているので、十分かなと。テスト失敗したときに行番号が表示されるなら、ESpec でも良かったんですけどね……。Elixir の練習のために admiral_stats_parseradmiral_stats_parser_ex に移植していて、作業が終わりかけたところでこれに気付いたときは愕然としました。

ちなみに、Elixir には、Ruby における Cucumber 相当の “WhiteBread” や、FactoryGirl 相当の “ExMachina” もあるようです。そのうち、これらも試してみたいと思います。