2015年4月30日
超高速なJSON APIをElixirフレームワークのPhoenixでビルドしてテストしよう
本記事は、原著者の許諾のもとに翻訳・掲載しております。
連絡先リストの役割を果たすJSON APIをビルドしてみましょう。ElixirとPhoenixを使って書きます。 Phoenix はElixirベースのフレームワークで、記述の高速化と、低遅延のWebアプリケーションをできる限り楽しく作成する目的で作られています。ElixirやPhoenixのインストール手順についてはここでは触れません。まず初めに Phoenixのガイド を読んでください。
なぜElixirとPhoenixなのか
Erlangは、ポンコツの板金にくるまれたフェラーリのようなものです。ものすごいパワーを持っていますが、見た目が悪いと感じる人が多いのです。WhatsAppはこれを使って膨大な数の接続を処理していますが、見慣れないシンタックスとツールの不足のせいで多くの人が苦労しています。 Elixir はそこを改善したものです。Erlang上に構築されていますが、美しくかつ楽しめるシンタックスを備え、 mix などのツールが、効率よくビルド、テスト、アプリケーションとの連携を促進してくれます。
Phoenixでは、Elixir上で楽しみながら非常に低遅延のWebアプリケーションを作成し構築することができます。超高速のアプリケーションと楽しめる開発環境は、もはや相互矛盾するものではありません。ElixirとPhoenixはその両方を与えてくれます。Phoenixのレスポンスタイムはミリ秒ではなくマイクロ秒で計測されることも少なくありません。
さて、このフレームワークを使ってビルドしてみたい理由をお話ししました。今度は実際にビルドしてみましょう。
テストを書く
PhoenixのWebサイト上の はじめに を参照して、 HelloPhoenix
という名前の新しいアプリを作成してみましょう。この演習にはPhoenix 0.10.0を使います。
Phoenixのアプリケーションが設定できたら、まずはテストを書いてみましょう。次のファイルを作成します。
test/controllers/contact_controller_test.exs
defmodule HelloPhoenix.ContactControllerTest do
use ExUnit.Case, async: false
use Plug.Test
alias HelloPhoenix.Contact
alias HelloPhoenix.Repo
alias Ecto.Adapters.SQL
setup do
SQL.begin_test_transaction(Repo)
on_exit fn ->
SQL.rollback_test_transaction(Repo)
end
end
test "/index returns a list of contacts" do
contacts_as_json =
%Contact{name: "Gumbo", phone: "(801) 555-5555"}
|> Repo.insert
|> List.wrap
|> Poison.encode!
response = conn(:get, "/api/contacts") |> send_request
assert response.status == 200
assert response.resp_body == contacts_as_json
end
defp send_request(conn) do
conn
|> put_private(:plug_skip_csrf_protection, true)
|> HelloPhoenix.Endpoint.call([])
end
end
まず、 setup
の関数を書きます。テストを始める時に常にデータベースが空であるということを保証するための処理の中で、Ecto callsをラップします。
テスト自体は、特別なものではありません。 use Plug.Test
によって、接続を生成する conn/2
関数にアクセスできます。このテストでは、新規の連絡先を1件挿入し、1つのリストにラップしてエンコードします。次に、新たな接続を生成し、リクエストを送ります。レスポンスの成功と、ボディがJSONにエンコードされた連絡先リストを含むことをアサートしましょう。
mix test
を実行すると、 HelloPhoenix.Contact. struct /0 is undefined, cannot expand struct HelloPhoenix.Contact
というエラーが表示されます。これは、まだモデルが生成されていないためです。 Ecto を用いて、Postgresデータベースに接続しましょう。
データベースを構築する
Ectoは、データベースのデータ保存と取り出しにリポジトリを使います。Phoenixにはあらかじめリポジトリがインストールされ、初期設定がなされています。 config/dev.exs
と config/test.exs
の実行で、Postgresのユーザとパスワードが正しいことを確認してください。
mix -h | grep ecto
の実行によって、Ectoから新規に取得した mix
のタスクを見てみましょう。
複数のタスクが使えることが分かったら、開発環境とテスト用データベースを構築しましょう。その後、最初のモデルを追加します。
# This will create your dev database
$ mix ecto.create
# This will create your test database
$ env MIX_ENV=test mix ecto.create
連絡先モデルを追加する
web/models/contact.ex
内の Contact
にスキーマを追加しましょう。
defmodule HelloPhoenix.Contact do
use Ecto.Model
schema "contacts" do
field :name
field :phone
timestamps
end
end
次に、 mix ecto.gen.migration create_contacts
でマイグレーションを作ります。新規生成したマイグレーションに、次のように記述します。
defmodule HelloPhoenix.Repo.Migrations.CreateContacts do
use Ecto.Migration
def change do
create table(:contacts) do
add :name
add :phone
timestamps
end
end
end
Ectoマイグレーションに初期設定されているコラムは、:stringです。その他のオプションは、 Ecto.Migration docs を参照してください。
では、 mix ecto.migrate
を実行して新規テーブルを生成し、もう一度 MIX_ENV=test mix ecto.migrate
でテストします。
ルートとコントローラを追加する
/api/contacts
といったルートを用いて、APIのエンドポイントにつなぎましょう。
# In our web/router.ex
defmodule HelloPhoenix.Router do
use Phoenix.Router
pipeline :api do
plug :accepts, ["json"]
end
scope "/api", HelloPhoenix do
pipe_through :api
resources "/contacts", ContactController
end
end
Railsを知っている人なら、 /api/contacts.json
が404 not foundを返すことに気づくでしょう。ここでは、適切なリクエストヘッダを設定する必要があります。ひとまず、 /api/contacts?format=json
で実行できますが、おすすめしません。パフォーマンスの観点からパラメータの文末指定は避けたいこと、またHTTPヘッダが既にその機能を備えていることがその理由です。
ただ、 mix test
を実行した後も、 ContactController
が必要です。
** (UndefinedFunctionError) undefined function: HelloPhoenix.ContactController.init/1 (module HelloPhoenix.ContactController is not available)
web/controllers/contact_controller.ex
でコントローラを作成しましょう。
defmodule HelloPhoenix.ContactController do
use HelloPhoenix.Web, :controller
alias HelloPhoenix.Repo
alias HelloPhoenix.Contact
plug :action
def index(conn, _params) do
contacts = Repo.all(Contact)
render conn, contacts: contacts
end
end
まず、 Repo.all(Contact)
の全てのコンタクトを用意してください。次に Phoenix.Controller.render/2
でJSONをレンダリングします。Use HelloPhoenix.Web, :controller
を呼び出せば、関数は自動的に取り込まれます。 Web/web.ex
をチェックして、他に何が呼び出されているかを確認してみましょう。
ここで Mix test
を実行しても、まだパスしません。
** (UndefinedFunctionError) undefined function: HelloPhoenix.ContactView.render/2 (module HelloPhoenix.ContactView is not available)
JSONをレンダリングするには、viewが必要なのです。
viewでJSONをレンダリングする
viewはJSONの出力方法を処理します。今現在これは非常にシンプルな操作ですが、将来的には例えば、ユーザのパーミッションに基づいて、送信するものを変更することもできます。
では Web/views/contact_view.ex
にファイルを作成しましょう。
defmodule HelloPhoenix.ContactView do
use HelloPhoenix.Web, :view
def render("index.json", %{contacts: contacts}) do
contacts
end
end
これはパターンマッチングを使用して設定し、 contacts
を返します。PhoenixはJSONへのコンタクトの配列を自動的にエンコードします。このviewの関数を使ってJSONの表示方法をカスタマイズすることができますが、これについては、また別の記事で説明します。
ここで mix test
を実行すれば、全てのテストをパスするでしょう。
クリーンアップ
web/web.ex
内の HelloPhoenix.Web
をチェックして、もう少しアプリケーションをクリーンアップしてみましょう。このファイルを開くと、既にコントローラの関数には HelloPhoenix.Repo
のエイリアスがあることが分かります。
def controller do
quote do
# Auto generated - This imports all the macros and functions that a controller needs.
use Phoenix.Controller
# Auto inserted - The app was generated with an alias to Repo as a convenience.
alias HelloPhoenix.Repo
# This imports the router helpers so you can generate paths like
# `api_contacts_path(conn)`
import HelloPhoenix.Router.Helpers
end
end
つまり、コントローラで HelloPhoenix.Repo
のエイリアスを取り除けるということです。
ExUnit.CaseTemplate を使って、テストを少しクリーンアップします。 test/test_helper.exs
では次のようになります。
# Add this above `ExUnit.start`
defmodule HelloPhoenix.Case do
use ExUnit.CaseTemplate
alias Ecto.Adapters.SQL
alias HelloPhoenix.Repo
setup do
SQL.begin_test_transaction(Repo)
on_exit fn ->
SQL.rollback_test_transaction(Repo)
end
end
using do
quote do
alias HelloPhoenix.Repo
alias HelloPhoenix.Contact
use Plug.Test
# Remember to change this from `defp` to `def` or it can't be used in your
# tests.
def send_request(conn) do
conn
|> put_private(:plug_skip_csrf_protection, true)
|> HelloPhoenix.Endpoint.call([])
end
end
end
end
using
にコードを追加すると、これらの関数とエイリアスは全てのテストで利用可能になります。なので、 send_request/1
とテストの他のエイリアスを取り除き、 use HelloPhoenix.Case
で置き換えることができます。
defmodule HelloPhoenix.ContactControllerTest do
use HelloPhoenix.Case, async: false
# We removed the other aliases since they're already included in
# `HelloPhoenix.Case`. We also removed the `setup` macro.
test "/index returns a list of contacts" do
contacts_as_json =
%Contact{name: "Gumbo", phone: "(801) 555-5555"}
|> Repo.insert
|> List.wrap
|> Poison.encode!
response = conn(:get, "/api/contacts") |> send_request
assert response.status == 200
assert response.resp_body == contacts_as_json
end
# We also removed the function definition for `send_request/1`
end
まとめ
PhoenixでJSON APIを作成して、テストする方法を見てきました。また、ファイルをクリーンアップして、将来的に HelloPhoenix.Web
と EXUnit.CaseTemplate
を使うことで、他のコントローラとテストでモジュールの利用を簡単にする方法を説明しました。これで、このアプリケーションを Elixir buildpac でHerokuに展開することができます。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa