Elixir + Phoenix + ESpecでBDDするまで

# 前置き

この記事は[Elixir Advent Calendar 2016](http://qiita.com/advent-calendar/2016/elixir)の8日目の記事です。

Ruby + Rails + RSpecを使ったウェブアプリケーション開発は定番だと思いますが、[Elixir](http://elixir-lang.org/) + [Phoenix](http://www.phoenixframework.org/) + [ESpec](https://github.com/antonmi/espec)の日本語の情報が少ないようなので書きます。

ソースは[こちら](https://github.com/techno-tanoC/elixir_phoenix_espec)

## 環境作り
### Erlang, Elixir, Node

Erlang, Elixir, Nodeのバージョン管理を[asdf](https://github.com/asdf-vm/asdf)の1つで行えるので重宝しています。

asdfを使わない場合は[Installing Elixir](http://elixir-lang.org/install.html)を参考にどうぞ。

### Phoenix

インストールは[公式サイト](https://hexdocs.pm/phoenix/installation.html#phoenix)にあるように

```bash
$ mix archive.install hex phx_new 1.5.5
```

でok

### PostgreSQL

ubuntuであればapt、macであればhomebrewで入ると思います。

僕はdocker-composeで入れて動かしています。

```docker-compose.yml
postgres:
  image: postgres
  environment:
    POSTGRES_USER: postgres
    POSTGRES_PASSWORD: postgres
  volumes:
    - ./docker/data:/var/lib/postgresql/data/
  ports:
    - 5432:5432
```

volumesはお好みで。

## 動作環境

* Ubuntu16.10
* Elixir 1.3.4
* Phoenix 1.2.1
* Erlang 19.1
* Node.js 7.2
* PostgreSQL 9.6.1

# Phoenixアプリケーションの準備

```bash
$ mix phoenix.new my_app
```

`Fetch and install dependencies?`と聞かれますが、`y`でokです。`y`だと`mix deps.get`と`npm install`をしてくれます。

作られたプロジェクトに移動して

```bash
$ mix ecto.create
$ mix phoenix.server
```

PostgreSQLのusername, passwordの設定が必要な場合は`config/dev.exs`を書き換えてください。

`mix phoenix.server`でサーバが立ち上がるので`localhost:4000`をブラウザで開くとWelcomeページが見えると思います。

![Screenshot from 2016-12-06 14-07-10.png](https://qiita-image-store.s3.amazonaws.com/0/49469/1c060f56-7dcc-6b98-c191-3f67d3e923f4.png)

# HTMLリソースを作る

タイトルに「BDD」と書いたので先にテストを書くべきなのでしょうが、分かりやすさを優先して先に実装を行います(タイトル詐欺)
今回は`Article`というリソースを作ってみましょう。`mix phoenix.gen.html`タスクを使います。

このタスクはマイグレーション、モデル、コントローラー、ビュー、テンプレートを一度に作ってくれます。railsで言うところのscaffoldです。ちなみにjson apiを作るための`phoenix.gen.json`というものもあります。

```bash
$ mix phoenix.gen.html Article articles title:string body:string
```

モデルの単数形と複数形とカラムを指定します。詳しくは[Mix Tasks · Phoenix](http://www.phoenixframework.org/docs/mix-tasks)をどうぞ

```
Add the resource to your browser scope in web/router.ex:

    resources "/articles", ArticleController

Remember to update your repository by running migrations:

    $ mix ecto.migrate
```

と言われるので指示に従います。

```web/router.ex
scope "/", MyApp do
  pipe_through :browser

  get "/", PageController, :index
  resources "/articles", ArticleController # ここを追加
end
```

```bash
$ mix ecto.migrate
```

サーバを再起動して`/articles`を開いて遊んでみてください。

![Screenshot from 2016-12-06 14-28-29.png](https://qiita-image-store.s3.amazonaws.com/0/49469/e513eb3d-3759-a970-185b-9052d0310cc5.png)

![Screenshot from 2016-12-06 14-28-41.png](https://qiita-image-store.s3.amazonaws.com/0/49469/0668aa16-9394-d726-c7d0-17fe14caabe7.png)

![Screenshot from 2016-12-06 14-28-19.png](https://qiita-image-store.s3.amazonaws.com/0/49469/91690bb2-d899-92cf-d333-2822aba9a18c.png)

# ESpec, ESpecPhoenix導入

一応説明しておくと[ESpec](https://github.com/antonmi/espec)はRSpecインスパイアのテスティングフレームワーク、[ESpecPhoenix](https://github.com/antonmi/espec_phoenix)はPhoenixのテストを書く際に必要なディレクティブやヘルパーを導入してくれるライブラリです。

## ESpec, ESpecPhoenixのセットアップ

`mix.exs`のdepsにespecとespec_phoenixを追加します。

```config.exs
def deps do
  ...
  {:espec, "~> 1.2.0", only: :test},
  {:espec_phoenix, "~> 0.6.4", only: :test}
  ...
end
```

`mix espec`実行時に`MIX_ENV=test`を付けなくてもよくするための設定を`config.exs`に追加

```config.exs
def project do
  ...
  preferred_cli_env: [espec: :test],
  ...
end
```

ダウンロードとESpce,ESPecPhoenixの導入とテスト用のデータベース作成

```bash
$ mix deps.get
$ MIX_ENV=test mix espec.init
$ MIX_ENV=test mix espec_phoenix.init
$ MIX_ENV=test mix ecto.create
```

この時点で`mix espec`を実行すると0件ですがespecが動くと思います。

```bash
$ mix espec


	0 examples, 0 failures

	Finished in 0.01 seconds (0.01s on load, 0.0s on specs)

	Randomized with seed 25252
```

## PhoenixESpecの設定

`spec/spec_helper.exs`を書き換えます。

```spec/spec_helper.exs
Code.require_file("#{__DIR__}/phoenix_helper.exs")

ESpec.configure fn(config) ->
  config.before fn(_tags) ->
    :ok = Ecto.Adapters.SQL.Sandbox.checkout(MyApp.Repo)
  end

  config.finally fn(_shared) ->
    Ecto.Adapters.SQL.Sandbox.checkin(MyApp.Repo, [])
  end
end
```

`MyApp.Repo`の部分は作ったプロジェクトのものにしてください。`mix phoenix.new my_app`として作った場合は`MyApp.Repo`です。

# Specを書く
ではいよいよ作ったArticleに対してスペックを書いていきます。RSpecを使ったことがある人ならあまり苦労することなくESpecを使えると思います。

注意として、スペックを書くファイルは必ず`_spec.exs`で終わる名前にしてください。

関数型らしく、割とパラメータは明示的なのでテストしやすい印象。

## ModelSpec

```spec/models/article_spec.exs
defmodule MyApp.ArticleSpec do
  use ESpec.Phoenix, model: Article, async: true
  alias MyApp.Article

  subject do: Article.changeset(%Article{}, attrs).valid?

  describe "changeset" do
    context "with valid attributes" do
      let :attrs, do: %{title: "国木田花丸", body: "未来ずら"}
      it do: is_expected |> to(be_true)
    end
  
    context "with invalid attributes" do
      let :attrs, do: %{}
      it do: is_expected |> to(be_false)
    end
  end
end
```

Phoenix(正確にはDBラッパーであるEcto)でのレコードの作成、更新は基本的にChangesetを経由して行います。従って、Changesetが正しく作られることをテストしています。

「ModelはRepoをDIすることによりModelの関数はできるだけ純粋にする」という思想がPhoenixにはある気がします(要出典)。

## ControllerSpec

全部の関数のテストをしたいところですが執筆時点で時間がないので`show`と`create`だけ適当に書き殴ります。

```spec/controllers/article_controller_spec.exs
defmodule MyApp.ArticleControllerSpec do
  use ESpec.Phoenix, controller: ArticleController
  alias MyApp.Repo
  alias MyApp.Article

  let :attrs, do: %{title: "国木田花丸", body: "未来ずら"}
  let :assigns, do: conn.assigns
  let :template, do: conn.private.phoenix_template

  describe "show" do
    context "when id is existent" do
      let! :article, do: Article |> struct(attrs) |> Repo.insert!
      let :conn, do: get build_conn, article_path(build_conn, :show, article.id)

      it "returns 200" do
        conn.status |> should(eq 200)
      end

      it "assigns chosen resource" do
        assigns.article |> should(eq article)
      end
    end

    context "when id is nonexistent" do
      let :conn, do: get build_conn, article_path(build_conn, :show, -1)
      it "returns 404" do
        assert_error_sent 404, fn -> conn end
      end
    end
  end

  describe "create" do
    context "with vlid attributes" do
      let :conn, do: post build_conn, article_path(build_conn, :create), article: attrs

      it "returns 302" do
        conn.status |> should(eq 302)
      end

      it "redirects to index page" do
        assert redirected_to(conn) == article_path(build_conn, :index)
      end

      it "creates a article" do
        expect(&conn/0) |> to(change(fn -> Article |> Repo.all |> Enum.count end, 0, 1))
      end
    end
 
    context "with invalid attributes" do
      let! :conn, do: post build_conn, article_path(build_conn, :create), article: %{}

      it "renders new template" do
        template |> should(eq "new.html")
      end

      it "assigns invalid changeset" do
        assert assigns.changeset
      end

      it "doesn't create articles" do
        expect(&conn/0) |> to_not(change(fn -> Article |> Repo.all |> Enum.count end))
      end
    end
  end
end
```

PhoenixのControllerはあくまでも「connとリクエストパラメータを受け取って、connを返す関数」なので、必要な情報は大体connに入っています。

createした時のHTTPステータスコードが302なのにはちょっと驚きました。

## ViewSpec

こちらも代わり映えしないので`show`だけ・・・

```spec/views/article_view_spec.exs
defmodule MyApp.ArticleViewSpec do
  use ESpec.Phoenix, view: MyApp.ArticleView, async: true
  alias MyApp.Article

  let :article, do: %Article{id: 1, title: "国木田花丸", body: "未来ずら" }

  describe "show" do
    let :response, do: render_to_string(@view, "show.html", conn: build_conn, article: article)
    it "renders show template" do
      assert response =~ "国木田花丸"
      assert response =~ "未来ずら"
    end
  end
end
```

テンプレートをrenderしたの結果を見ています。

## RequestSpec

(書く時間が無いので)[むーりぃー…](http://dic.pixiv.net/a/%E6%A3%AE%E4%B9%85%E4%BF%9D%E4%B9%83%E3%80%85)

`get`とか`post`して`conn.resp_body`とかを見れば良いんじゃないでしょうか。json apiの時はrequest specかview specはガッツリ書きたいところです。

# 最後に
以上、Phoenixアプリの作成からテストを書くところまででした。関数型言語だけあって明示的に引数を渡し、それに対応した結果が返ってくるので分かりやすい仕組みだと思います。(データベースの更新などは思いっきり副作用ですがしょうがないですね)

ESpecらしいところの1つにasyncオプションがあるところがあります。今回のテストぐらいの量では誤差レベルの差しかありませんが、もっと多くなってくると大きな差になると思います。ただし、グローバルな状態を持つ処理はasyncにできないので、できるだけ副作用を局所化し、純粋な関数を書くように心がけましょう。

疲れた・・・[魂が…消えますけど…](http://dic.nicovideo.jp/a/%E6%A3%AE%E4%B9%85%E4%BF%9D%E4%B9%83%E3%80%85)。