2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

PhonenixによるWeb開発 - 雛形テンプレート作りとCIによるコードレビューとテスト実行まで -

Posted at

はじめに

以前までのPhoenixに関する記事は以下の2つです。

もう少しPhoenixによるWebアプリケーション開発に慣れたいので、今回は実際にmixコマンドを用いてWebリソースを生成するための方法とコードレビュー、テスト実行まで纏めたものを整理します。

Phoenixのテンプレートをカスタマイズ - UI上でログインしているかどうか確認することができる -

まずはデフォルトのlayout/app.html.eexをカスタマイズしてみましょう。

PhoenixのデフォルトのCSSフレームワークですが、milligramというものを使っている模様です。

layout/app.html.eexのヘッダ部分を変えてみます。

layout/app.html.eex
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>DailyReport · Phoenix Framework</title>
    <link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
  </head>
  <body>
    <header>
      <%= render DailyReportWeb.SharedView, "header.html", assigns %>
    </header>
    <main role="main" class="container">
      <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
      <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
      <%= render @view_module, @view_template, assigns %>
    </main>
    <script type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
  </body>
</html>

ヘッダー部分をpartialとして分けたいため、別ページとして作成することにしました。

% mkdir shared
% vim shared/header.html.eex
shared/header.html.eex
<section class="container">
  <nav role="navigation">
    <ul>
      <%= if logged_in?(@conn) do %>
        <li><a class="button" href="<%= Routes.session_path(@conn, :logout) %>">ログアウト</a></li>
      <% end %>
    </ul>
  </nav>
</section>

header.html.eexの中でlogged_inというメソッドを用意します。中身は自分で作成しますが、logged_inは現在ログイン中かどうか確認するためのメソッドの予定です。
さて、実際の中身をどうするかについては、(1)
(2)の参考文献を踏まえて考えました。今回は、Viewから実行するメソッドなためController側で行うよりもModel側で実装することに決めました。

lib/daily_report/user_manager/guardian.ex
  def current_user(conn), do: Guardian.Plug.current_resource(conn, [])

  def logged_in?(conn), do: Guardian.Plug.authenticated?(conn, [])

そしてこれをhelperメソッドとして扱えるようにview側でimportします。

lib/daily_report_web.ex
  def view do
    quote do
      import DailyReport.UserManager.Guardian, only: [current_user: 1, logged_in?: 1]
    end
  end
lib/daily_report/user_manager/guardian.ex
defmodule DailyReport.UserManager.Guardian do
  def current_user(conn), do: Guardian.Plug.current_resource(conn, [])

  def logged_in?(conn), do: Guardian.Plug.authenticated?(conn, [])
end

Guardian.Plugモジュールにより、認証に関係する関数が提供されるため、上記2つの関数を使うことにしました。

雛形テンプレートの作成

ユーザの投稿記事機能を作る

ログインした後の最初のページは、自分が投稿した記事の一覧を閲覧できるようなページにしたいので、一旦User以外のリソース、投稿記事に対応するReportモデルを作ることとしました。
なお、PhoenixでもRailsのようにscaffoldのような雛形のテンプレートを作成することができます。

docker exec daily_report_web_1 mix phx.gen.html Articles Article article title:string body:text

また、User : Articlesの関係を1対N対応にしたいため、migrationファイルやモデル構造の書き換えを行います。

lib/daily_report/articles/article.ex
defmodule DailyReport.Articles.Article do
  use Ecto.Schema
  import Ecto.Changeset

  schema "article" do
    field :body, :string
    field :title, :string
    belongs_to :user, DailyReport.UserManager.User
    timestamps()
  end

  @doc false
  def changeset(article, attrs) do
    article
    |> cast(attrs, [:title, :body])
    |> validate_required([:title, :body])
  end
end
lib/daily_report/user_manager/user.ex
defmodule DailyReport.UserManager.User do
  use Ecto.Schema
  import Ecto.Changeset
  alias DailyReport.UserManager.User

  schema "users" do
    field :password, :string
    field :username, :string

    timestamps()
    has_many :articles, DailyReport.Articles.Article
  end
priv/repo/migrations/20200222064817_create_article.exs
defmodule DailyReport.Repo.Migrations.CreateArticle do
  use Ecto.Migration

  def change do
    create table(:article) do
      add :title, :string
      add :body, :text
      add :user_id, references(:users)
      timestamps()
    end

  end
end
% docker exec daily_report_web_1 mix ecto.migrate                                      (git)-[feature/add_daily_report]

06:56:20.864 [info]  == Running 20200222064817 DailyReport.Repo.Migrations.CreateArticle.change/0 forward

06:56:20.898 [info]  create table article

06:56:20.982 [info]  == Migrated 20200222064817 in 0.0s
lib/daily_report_web/router.ex
  scope "/", DailyReportWeb do
    pipe_through [:browser, :auth, :ensure_auth]
    resources "/article", ArticleController
  end

ログイン成功後、及びloginページに遷移した時にartice/index.htmlにリダイレクトできるように修正します。

lib/daily_report_web/controllers/session_controller.ex

  def new(conn, _) do
    changeset = UserManager.change_user(%User{})
    maybe_user = Guardian.Plug.current_resource(conn)
    if maybe_user do
      redirect(conn, to: "/article")
    else
      render(conn, "new.html", changeset: changeset, action: Routes.session_path(conn, :login))
    end
  end

  defp login_reply({:ok, user}, conn) do
    conn
    |> put_flash(:info, "DailyReportにログインしました!")
    |> Guardian.Plug.sign_in(user)
    |> redirect(to: "/article")
  end

以上で、ログイン時に投稿記事一覧にリダイレクトすることが可能となりました。

投稿記事一覧.png

TravisCIの設定

雛形テンプレート作成を学んだので、ここから本格的にコードレビューやテスト実行をCIサービスで行えるようにしていきます。CIサービスはパブリックリポジトリであれば無料で使えるTravis CIを利用します。

.travis.yml
language: elixir
sudo: false
otp_release:
  - 22.2.1
elixir:
  - 1.9.4
services:
  - postgresql
addons:
  postgresql: "10"
  apt:
    packages:
    - postgresql-10
    - postgresql-client-10
env:
  global:
  - PGPORT=5432
  - MIX_ENV=test
cache:
  directories:
    - _build
    - deps
before_script:
  - psql -c 'create database travis_ci_test;' -U postgres
  - cp config/travis.exs config/test.exs
  - mix do ecto.create, ecto.migrate
script:
  - mix credo --strict
  - mix dialyzer --halt-exit-status
  - mix test

今回はDBの設定も必要となるので、必要に応じてpostgresqlに接続するconfigファイルも生成しました。

config/travis.exs
use Mix.Config

# Configure your database
config :daily_report, DailyReport.Repo,
  username: "postgres",
  password: "",
  database: "travis_ci_test",
  hostname: "localhost",
  pool: Ecto.Adapters.SQL.Sandbox

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

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

CIによるコードレビュー

コードレビュー関係のツールにはcredo(5)dialyxir(6)を利用しました。この辺りは機械的でも良いからレビュー案が欲しいので詳しく調べるのは一旦省略。
なお、ここの設定部分に関しては(7)
を参考にしました。

CIによるテスト実行

前述のコードレビューと同時に、CIによるテストも行います。
テストのテンプレートに関してはphx.gen.htmlタスクコマンドで概ねなものは作ってくれます。
ただし、articleページに関しては、Guardianによるログイン認証を先に行わないと閲覧できないステートにしているため、テストでも予めアクセスする前にユーザログインを済ましておく必要があります。

Phoenixのテストでは、Tag付けによって、条件分岐をすることができるので、conn_case.exを(9)のようにカスタマイズすることで、sign_inしたい時とそうでない時で分岐をすることとしました。

test/support/conn_case.ex
defmodule DailyReportWeb.ConnCase do
  @moduledoc """
  This module defines the test case to be used by
  tests that require setting up a connection.

  Such tests rely on `Phoenix.ConnTest` and also
  import other functionality to make it easier
  to build common data structures and query the data layer.

  Finally, if the test case interacts with the database,
  we enable the SQL sandbox, so changes done to the database
  are reverted at the end of every test. If you are using
  PostgreSQL, you can even run database tests asynchronously
  by setting `use DailyReportWeb.ConnCase, async: true`, although
  this option is not recommendded for other databases.
  """
  use ExUnit.CaseTemplate
  alias DailyReport.{UserManager, UserManager.Guardian}

  using do
    quote do
      # Import conveniences for testing with connections
      use Phoenix.ConnTest
      alias DailyReportWeb.Router.Helpers, as: Routes
      # The default endpoint for testing
      @endpoint DailyReportWeb.Endpoint
    end
  end

  @default_opts [
    store: :cookie,
    key: "secretkey",
    encryption_salt: "encrypted cookie salt",
    signing_salt: "signing salt"
  ]
  @signing_opts Plug.Session.init(Keyword.put(@default_opts, :encrypt, false))
  @valid_user %{password: "some password", username: "some username"}

  setup tags do
    :ok = Ecto.Adapters.SQL.Sandbox.checkout(DailyReport.Repo)

    unless tags[:async] do
      Ecto.Adapters.SQL.Sandbox.mode(DailyReport.Repo, {:shared, self()})
    end

    {conn} = if tags[:authenticated] do
      {:ok, user} = UserManager.create_user(@valid_user)

      conn = Phoenix.ConnTest.build_conn()
      |> Plug.Session.call(@signing_opts)
      |> Plug.Conn.fetch_session
      |> Guardian.Plug.sign_in(user)
      {conn}
    else
      {Phoenix.ConnTest.build_conn()}
    end
  
    {:ok, conn: conn}
  end
end

このタグ名をtestシナリオを実行する前にタグ付けすることで、sign_inの挙動を付与することができます。

test/controllers/article_controller_test.exs
defmodule DailyReportWeb.ArticleControllerTest do
  use DailyReportWeb.ConnCase
  alias DailyReport.Articles
  @create_attrs %{body: "some body", title: "some title"}
  @update_attrs %{body: "some updated body", title: "some updated title"}
  @invalid_attrs %{body: nil, title: nil}
  def fixture(:article) do
    {:ok, article} = Articles.create_article(@create_attrs)
    article
  end

  describe "index" do
    @tag :authenticated
    test "lists all article", %{conn: conn} do
      conn = get(conn, Routes.article_path(conn, :index))
      assert html_response(conn, 200) =~ "Listing Article"
    end
  end

  describe "new article" do
    @tag :authenticated
    test "renders form", %{conn: conn} do
      conn = get(conn, Routes.article_path(conn, :new))
      assert html_response(conn, 200) =~ "New Article"
    end
  end

  describe "create article" do
    @tag :authenticated
    test "redirects to show when data is valid", %{conn: conn} do
      conn = post(conn, Routes.article_path(conn, :create), article: @create_attrs)

      assert %{id: id} = redirected_params(conn)
      assert redirected_to(conn) == Routes.article_path(conn, :show, id)

      conn = get(conn, Routes.article_path(conn, :show, id))
      assert html_response(conn, 200) =~ "Show Article"
    end

    @tag :authenticated
    test "renders errors when data is invalid", %{conn: conn} do
      conn = post(conn, Routes.article_path(conn, :create), article: @invalid_attrs)
      assert html_response(conn, 200) =~ "New Article"
    end
  end

  describe "edit article" do
    setup [:create_article]
    @tag :authenticated
    test "renders form for editing chosen article", %{conn: conn, article: article} do
      conn = get(conn, Routes.article_path(conn, :edit, article))
      assert html_response(conn, 200) =~ "Edit Article"
    end
  end

  describe "update article" do
    setup [:create_article]
    @tag :authenticated
    test "redirects when data is valid", %{conn: conn, article: article} do
      conn = put(conn, Routes.article_path(conn, :update, article), article: @update_attrs)
      assert redirected_to(conn) == Routes.article_path(conn, :show, article)

      conn = get(conn, Routes.article_path(conn, :show, article))
      assert html_response(conn, 200) =~ "some updated body"
    end

    @tag :authenticated
    test "renders errors when data is invalid", %{conn: conn, article: article} do
      conn = put(conn, Routes.article_path(conn, :update, article), article: @invalid_attrs)
      assert html_response(conn, 200) =~ "Edit Article"
    end
  end

  describe "delete article" do
    setup [:create_article]
    @tag :authenticated
    test "deletes chosen article", %{conn: conn, article: article} do
      conn = delete(conn, Routes.article_path(conn, :delete, article))
      assert redirected_to(conn) == Routes.article_path(conn, :index)
      assert_error_sent 404, fn ->
        get(conn, Routes.article_path(conn, :show, article))
      end
    end
  end

  defp create_article(_) do
    article = fixture(:article)
    {:ok, article: article}
  end
end

以上やcredo,dialyzerの設定でignoreなテストを設定すればテストがすべてパスします。
スクリーンショット 2020-02-24 22.13.58.png

まとめ

まだ、本格的にElixirのコードを書いた訳じゃありませんが、一旦これでPhoenixによるWeb開発の設定やテンプレート作りは一先ず終わったような気がします。
ここからは、本格的にゴリゴリElixirのコードを書いていき、まとめたものを執筆していこうと思います。
それでは。

参考文献

  1. https://ruby-rails.hatenadiary.com/entry/20151013/1444662887
  2. https://nabinno.github.io/f/2018/05/20/%E9%80%A3%E8%BC%89_rails2phoenix_2.html
  3. https://hexdocs.pm/guardian/Guardian.Plug.html
  4. https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Html.html
  5. https://github.com/rrrene/credo
  6. https://github.com/jeremyjh/dialyxir
  7. https://medium.com/@devenney/phoenix-framework-travis-ci-a46a0dbfecd7
  8. https://qiita.com/ak-ymst/items/1a973dd4b21c3d5ea4e1
  9. https://medium.com/@simon.strom/how-to-test-controller-authenticated-by-guardian-in-elixir-phoenix-b9bfa141ed4
2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?