POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

Paul Smith

本記事は、原著者の許諾のもとに翻訳・掲載しております。

連絡先リストの役割を果たす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.exsconfig/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.WebEXUnit.CaseTemplate を使うことで、他のコントローラとテストでモジュールの利用を簡単にする方法を説明しました。これで、このアプリケーションを Elixir buildpac でHerokuに展開することができます。