ExUnit入門

初学者向けです。ExUnitというテストフレームワークがあることは知っているけれど、使い方はあんまり分かっていない、という方を対象に書きます。

# テストを書く

## 基本の流れ

```ex
# 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](https://github.com/jjh42/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`のドキュメント](http://elixir-lang.org/docs/stable/ex_unit/ExUnit.Assertions.html)をあたってもらうとして、最もよく使うのは`assert/1`マクロです。`assert/1`に任意の式を与えると、以下のようにパースしてエラーを表示してくれます。

```elixir
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`マクロを使うと、テスト実行後の処理を記述できます。

```ex
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`の第２引数に渡ってきます。

```ex
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`は使えません

```ex
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を用いると、この使用例が実装と合っているかテストすることができます。

```ex
defmodule MyApp.MyModule do

  @doc """
  この関数は必ず1を返します

      iex> MyApp.MyModule.one
      1
  """
  def one, do: 1
end
```

上のようなモジュールとドキュメントがある場合、テストコードで`doctest`にモジュール名を指定することで、ドキュメント内のコードをテストできます。

```ex
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`を使うことで、テストにタグを付与することができます。テストにおいては、それを指定することで特定のテストを実行、またはスキップすることができます。

```ex
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`としてもスキップされます。

```ex
@tag :skip
test "always skipped" do
  ...
end
```

### 小ネタ

`lib`と`test`以下を監視して、変更があったらテストを走らせます。`--stale`はお好みで。

```
fswatch lib test | mix test --listen-on-stdin --stale
```

**追記**

watchするならこれがおすすめ。 https://github.com/lpil/mix-test.watch