Elixir+Phoenixプロジェクトで単体テストを行う

# やること
[これまで作ったデモアプリ](http://qiita.com/ak-ymst/items/30e7460432ab344534bf)のテストを確認する.
プロジェクトのソースは[こちら](https://github.com/ak-ymst/phoenix_sample)にあります.

# Elixirの単体テストフレームワークExUnit超入門
## プロジェクト作成からテスト実行まで
まず, デフォルトのElixirプロジェクトを作る.

```
$ mix new hoge_project
```

すると以下のような構成のディレクトリが作られると思う.

```
$ tree -L 1
.
├── README.md
├── _build
├── config
├── lib
├── mix.exs
└── test
```

このlibディレクトリ以下に, 新たに作成するモジュールを格納し, testディレクトリ以下には
それらのモジュールに対するテストモジュールを格納する.

例えば、すごくつまらない例になるが, 足し算機能を提供するモジュールとそれに対するテストを用意したい場合, libディレクトリにCalcモジュールを作成し, textディレクトリにそれに対するCalcTestモジュールを作ることになる.

```lib/calc.ex
defmodule Calc do

  def add(x, y) do
    x + y
  end

end
```

```test/calc_test.ex
defmodule CalcTest do
  use ExUnit.Case

  setup_all do
    IO.puts "now setup_all"
  end

  setup do
    IO.puts "now setup"
    {:ok, three: 3, four: 4}
  end

  test "Calc 1 + 2" do
     assert Calc.add(1, 2) == 3
  end

  test "try failed" do
     assert Calc.add(1, 2) == 4
  end

  test "use setuped context", context do
     assert Calc.add(context[:three], context[:four]) == 7
  end

end
```

テスト実行はmixコマンドのtestタスクで行う.
実際にテストをしてみると...

```
$ mix test
.now setup_all
now setup
now setup
.now setup


  1) test try failed (CalcTests)
     test/calc_test.exs:17
     Assertion with == failed
     code: Calc.add(1, 2) == 4
     lhs:  3
     rhs:  4
     stacktrace:
       test/calc_test.exs:18
                                                                                                                                                                                                 
.                                                                                                                                                                                                
                                                                                                                                                                                                 
Finished in 0.1 seconds (0.1s on load, 0.01s on tests)                                                                                                                                           
4 tests, 1 failure                                                                                                                                                                               
                                                                                                                                                                                                 
Randomized with seed 905035                                                                                                                                                                      
$ 

```

このようにテストが実行されて, 1つのテストに失敗している結果が表示される.
ちなみに, `mix test <path>`で指定したファイルのみのテストもできる.


## テストモジュールをもう少し詳しく見てみる
まず, ExUnit.Caseをロードする.

```
  use ExUnit.Case
```

これでテスト用の各種マクロが使えるようになる.
テストの定義は`test message do ~ `の部分

```
  test "Calc 1 + 2" do
     assert Calc.add(1, 2) == 3
  end
```

この構文はRubyの単体テストフレームワークにもあったと思うので, そちらを使ったことがある人には
違和感なく使えるんじゃないでしょうか.
使えるassertionは[こんな感じ] (http://elixir-lang.org/docs/stable/ex_unit/ExUnit.Assertions.html)

各種テスト前に共通処理を差し込みたい場合は, setupで定義する

```
  setup_all do
    IO.puts "now setup_all"
  end

  setup do
    IO.puts "now setup"
    {:ok, three: 3, four: 4}
  end

```

setupで定義された処理は各テストごとに, setup_allで定義された処理は最初に一回だけ実行される.
また

```
  setup do
    IO.puts "now setup"
    {:ok, three: 3, four: 4}
  end
```

setupでこのようなdictionaryを返すと,

```
  test "use setuped context", context do
     assert Calc.add(context[:three], context[:four]) == 7
  end
```

このように, テストのほうではcontext経由で値を取得することができる.
なお, teardownは存在しないが, かわりにon_exitという関数があるようだ。（挙動がよくわからなかったが）
これらについての詳細も, [こちら](http://elixir-lang.org/docs/stable/ex_unit/ExUnit.Callbacks.html)を読んでください.

# Phoenixプロジェクトでの単体テスト
## テストの準備
Phoenixプロジェクトでは, 単体テストは単体テスト用の環境でテストが行われる.
使用される設定ファイルは, `test/config/test.exs`なので、まずこれをを編集する.

```ex:test/config/test.exs
use Mix.Config

# We don't run a server during test. If one is required,                                                                                                                                         
# you can enable the server option below.                                                                                                                                                        
config :phoenix_sample, PhoenixSample.Endpoint,
  http: [port: 4001],
  server: false

# Print only warnings and errors during test                                                                                                                                                     
config :logger, level: :warn

# Configure your database                                                                                                                                                                        
config :phoenix_sample, PhoenixSample.Repo,
  adapter: Ecto.Adapters.MySQL,
  username: "phoenix",
  password: "phoenix",
  database: "phoenix_sample_test",
  hostname: "localhost",
  pool: Ecto.Adapters.SQL.Sandbox
```

とりあえず, 使用するDBとユーザのみを変えておく.
なお, ここで指定したDBの作成とマイグレーションはテスト実行前に呼び出される
`test/test_hepler.ex`にて行われるので, 事前に用意する必要はない.

```ex:test/test_helper.exs
ExUnit.start

Mix.Task.run "ecto.create", ~w(-r PhoenixSample.Repo --quiet)
Mix.Task.run "ecto.migrate", ~w(-r PhoenixSample.Repo --quiet)
Ecto.Adapters.SQL.begin_test_transaction(PhoenixSample.Repo)
                                               
```

あとは, `mix test`としてやればテストは実行されるでしょう.


## モデルのテスト
mixのジェネレータでモデルを作成すると、それに対応するテストモジュールも`test/modes/`以下に生成される.
Postモデルを作成した時に, 実際に生成されたテストを見てみると, 以下の様な内容だった.

```ex:test/models/post_test.exs
defmodule PhoenixSample.PostTest do
  use PhoenixSample.ModelCase

  alias PhoenixSample.Post

  @valid_attrs %{content: "some content", title: "some content"}
  @invalid_attrs %{}

  test "changeset with valid attributes" do
    changeset = Post.changeset(%Post{}, @valid_attrs)
    assert changeset.valid?
  end

  test "changeset with invalid attributes" do
    changeset = Post.changeset(%Post{}, @invalid_attrs)
    refute changeset.valid?
  end
end
```

デフォルトでは, valids関数を対象としたテストが作られています.
せっかくなので, 記事モデルに本文内容を先頭10文字に切り詰めた文字列を返す関数を追加してみることにします.

以下のテストを追加して

```ex:test/models/post_test.exs
  test "get truncated content" do
    post =  %Post{id: 1, title: "長い本文", content: "この記事は10文字以上の長さがあるよ"}
    assert Post.truncated(post.content) == "この記事は10文字以..."
  end

  test "get not truncated content" do
    post =  %Post{id: 2, title: "短い本文", content: "この記事は8文字"}
    assert Post.truncated(post.content) == "この記事は8文字"
  end
```

再度テストを実行すると...


```
$ mix test test/models/post_test.exs 


  1) test get truncated content (PhoenixSample.PostTest)
     test/models/post_test.exs:19
     ** (KeyError) key :truncated not found in: %PhoenixSample.Post{__meta__: #Ecto.Schema.Metadata<:built>, comments: #Ecto.Association.NotLoaded<association :comments is not loaded>,\
 content: "この記事は10文字以上の長さがあるよ", id: 1, inserted_at: nil, title: "長い本文", updated_at: nil}
     stacktrace:
       test/models/post_test.exs:21

.

  2) test get not truncated content (PhoenixSample.PostTest)
     test/models/post_test.exs:24
     ** (KeyError) key :truncated not found in: %PhoenixSample.Post{__meta__: #Ecto.Schema.Metadata<:built>, comments: #Ecto.Association.NotLoaded<association :comments is not loaded>,\
 content: "この記事は8文字", id: 2, inserted_at: nil, title: "短い本文", updated_at: nil}
     stacktrace:
       test/models/post_test.exs:26

.

Finished in 0.3 seconds (0.3s on load, 0.01s on tests)
4 tests, 2 failures

Randomized with seed 591922
```

当然, まだ関数は未定義なのでエラー.
記事モデルに関数を追加して

```ex:web/models/post.ex

  @doc """                                                                                                                                                                                       
    引数の文字列が10文字を超えていた場合、それを切り詰める                                                                                                                                       
  """
  def truncate(content) do
    if String.length(content) > 9 do
      String.slice(content, 0..9) <> "..."
    else
      content
    end
  end
```

再度テスト実行

```
$ mix test test/models/post_test.exs 
Compiled web/controllers/comment_controller.ex
....

Finished in 0.2 seconds (0.2s on load, 0.00s on tests)
4 tests, 0 failures

Randomized with seed 864661

```

今度は:ok

## DBを扱う
ここまでで, 関数とテストの追加はできたのですが, モデルのインスタンスを直接生成してしまっていていて面白くない.
なので, インスタンスの取得をDBから行うようにしたいと思う.

まずテストで, テスト対象をDBからデータを取得するように変える

```ex:test/models/post.ex
  test "get truncated content" do
#    post =  %Post{id: 1, title: "長い本文", content: "この記事は10文字以上の長さがあるよ"}                                                                                                      
    post = Repo.get!(Post, 1)
    assert Post.truncate(post.content) == "この記事は10文字以..."
  end

  test "get not truncated content" do
#    post =  %Post{id: 2, title: "短い本文", content: "この記事は8文字"}                                                                                                                         
    post = Repo.get!(Post, 2)
    assert Post.truncate(post.content) == "この記事は8文字"
  end
```

リポジトリは, テスト用に用意されているModelCaseモジュールの中でロードされているらしく, 特に追加の宣言は必要なく使える.
そして, テスト実行

```
$ mix test test/models/post_test.exs 
..

  1) test get truncated content (PhoenixSample.PostTest)
     test/models/post_test.exs:19
     ** (Ecto.NoResultsError) expected at least one result but got none in query:                                                                                                                
                                                                                                                                                                                                 
     from p in PhoenixSample.Post,                                                                                                                                                               
       where: p.id == ^1                                                                                                                                                                         
     
     stacktrace:
       (ecto) lib/ecto/repo/queryable.ex:57: Ecto.Repo.Queryable.one!/4

    ... 
```

データは未投入なので, 当然対象のレコードが見つからないというエラー.
データ投入用のsetup関数を追加してやる.

```ex:test/models/post_model.ex
  # initilize test data                                                                                                                                                                          
  setup do                                                                                                                                                                                       
    Repo.insert! %Post{id: 1, title: "長い本文", content: "この記事は10文字以上の長さがあるよ"}                                                                                                  
    Repo.insert! %Post{id: 2, title: "短い本文", content: "この記事は8文字"}                                                                                                                     
                                                                                                                                                                                                 
    :ok                                                                                                                                                                                          
  end                                                                                                                                                                                            
```

そして, テスト実行

```
$ mix test test/models/post_test.exs 
....

Finished in 0.3 seconds (0.3s on load, 0.07s on tests)
4 tests, 0 failures

Randomized with seed 857481
```

こちらも:ok

## EctoFixturesを使う
DBと連携したモデルのテストができるようになったのだが, データ投入にRepoの関数を直接呼び出しているので非常に記述がくどい.
できたらテストデータは他のファイルにまとめておきたい.
さいわい, [EctoFixtures](https://github.com/h4cc/awesome-elixir)というライブラリの開発が行われているようなので試しに使ってみることにする.

まずmix.exsの依存ライブラリにecto_fituresを追加し

```mix.exs
  defp deps do
    [{:phoenix, "~> 1.1.4"},
     {:mariaex, ">= 0.0.0"},
     {:phoenix_ecto, "~> 2.0"},
     {:phoenix_html, "~> 2.4"},
     {:phoenix_live_reload, "~> 1.0", only: :dev},
     {:gettext, "~> 0.9"},
     {:cowboy, "~> 1.0"},
     {:ecto_fixtures, github: "DockYard/ecto_fixtures"}] # <- 追加
  end
```

ecto_fixturesはhexでも公開されているが, バージョンが古いままなので, githubのリポジトリから直接取得することにする.
そして, `mix get.deps`実行

```
$ mix deps.get
* Getting ecto_fixtures (https://github.com/DockYard/ecto_fixtures.git)
Cloning into '/home/vagrant/phoenix_sample/deps/ecto_fixtures'...
remote: Counting objects: 587, done.        
remote: Total 587 (delta 0), reused 0 (delta 0), pack-reused 586        
Receiving objects: 100% (587/587), 84.08 KiB | 52.00 KiB/s, done.
Resolving deltas: 100% (312/312), done.
Checking connectivity... done.
Running dependency resolution
Dependency resolution completed
  uuid: 1.1.3
```

テストデータは`test/fixtures/`以下に配置するようなので, そこに以下の様なファイルを作成

```test/fixtures/posts.exs
posts model: PhoenixSample.Post, repo: PhoenixSample.Repo do
  long do
    title "長い本文"
    content "この記事は10文字以上の長さがあるよ"
  end
  short do
    title "短い本文"
    content "この記事は8文字"
  end
end
```

次にテストで, ライブラリをロードし

```ex:test/models/post_test.ex
 use EctoFixtures
```

テストデータを使いたいテストの頭にタグを追加

```ex:test/models/post_test.ex
  @tag fixtures: :posts
  test "get truncated content", %{data: data} do
```

この部分は多分マクロで何らかのsetupに展開されているのだと思います.
DBからデータ取得する際に指定するIDを, fixtureから取得するように変更する

```ex:test/models/post_test.ex
#    post = Repo.get!(Post, 1)
    post = Repo.get!(Post, data.posts.long.id)
```

テスト全体としては, 以下のようになる.

```ex:test/models/post_test.ex
  @tag fixtures: :posts
  test "get truncated content", %{data: data} do
    post = Repo.get!(Post, data.posts.long.id)
    assert Post.truncate(post.content) == "この記事は10文字以..."
  end
```

最後に, 今までデータ投入を行っていたsetup関数を削除して完成.
テストを実行してみると

```
$ mix test test/models/post_test.exs 
....

Finished in 0.4 seconds (0.3s on load, 0.1s on tests)
4 tests, 0 failures

Randomized with seed 723406
```

:ok
最終的に出来上がったテストは全体は[こちら](https://github.com/ak-ymst/phoenix_sample/blob/master/test/models/post_test.exs)を見てください.

## コントローラのテスト
コントローラも, モデルと同様にgeneratorで生成した時点で対応するテストが生成される.
なお, ここで生成されるテストはいわゆる機能テストと呼ばれるものになる（と思う）.
本当にコントローラ単体のテストが欲しい場合は, 依存先モデルをMockに差し替える仕組みやらなんやらを用意してください.

実際生成されるテストはこんな感じ.

```test/controllers/api_post_controller.ex
  setup %{conn: conn} do
    {:ok, conn: put_req_header(conn, "accept", "application/json")}
  end

  test "lists all entries on index", %{conn: conn} do
    conn = get conn, api_post_path(conn, :index)
    assert json_response(conn, 200)["data"] == []
  end
```

見ての通り, RouterHelperのapi_post_path関数を使って得たURIに対してGETリクエストを行い, 結果をjsonデコードして結果を比較しているテストです.
もっとも, 結果の比較対象が空のリストになっている点からもわかるように, あくまで雛形ですが。
他のCRUDについてもテストの雛形は作られるので, 真似して書いていけばテストを増やしていくのは難しいことではないと思われます.
結果の正しさの定義が容易なAPIのテストを行いたい場合は, こちらを使っていくのもいいのではないでしょうか.
(HTMLを返すインタフェイスのテストは正しさの定義が面倒過ぎていやになります...)

# 最後に
今回は、ExUnitという、いわゆるxUnitの系譜に連なるテストフレームワークを使いましたが、
これとは別に、ESpecというRubyのRSpecの影響を受けたテストフレームワークもあるようなので
機会があったそちらも試してみたいと思います

# 参考サイト
ExUnit : http://elixir-lang.org/docs/stable/ex_unit/ExUnit.html
EctoFixtures : https://github.com/DockYard/ecto_fixtures