概要
普段UZUMAKIではRailsのお仕事が多いのですが、ここではElixirベースのフレームワークであるPhoenixを使ったAPI開発のノウハウを共有していきたいと思っています。チュートリアルではあまり紹介されないテストの方法や、定番のライブラリの紹介・設定も説明します。
ElixirはErlang VM上で動く関数型言語で、RailsコミッターのJoséさんが作ったプログラミング言語です。
Elixirの特徴としては、
- 並列処理を安定的に実行できる
- 耐障害性が高い
- シンプルでとっつきやすい言語仕様
などが挙げられます。
詳しくは以下の辺りに分かり易く纏まっています。
Elixir
全てのElixirのコードは、隔離された軽量の実行スレッド(プロセスと呼ばれる)の中で動作し、メッセージを通して情報をやり取りします。 その軽量性により、同一のマシン内で数千のプロセスが 同時に ...
elixir-lang.jp
【メモ】私が愛する Elixir/Erlang の楽しさと辛さ - Taku's Teckブログ
元資料が素晴らしいためまとめる必要など全くないのですが、自己の整理のため、箇条書きにて書き起こししているものです Web開発のための優秀なツール完備 サーバ構成をシンプルに保てる コードをシンプルに保てる 夜眠れる なぜ流行らないのか? 学習コストが高い(と思われれている) × 納期が短い = 採用されない 学習コストが高い(と思われれている)・・並行処理、関数型、マクロと難しそうな概念を学ぶ必要がありそう Webシステムの開発ならば、学習コストは高くも低くもない 加えて、安全性が高く、生産性が高く、それ以外の性質は程良い →Elixirは学習コストのわりには恩恵が大きい 難しいのであれば学ぶことを取捨選択して学習コストを下げる 以下、選択して学習することで学習コストを下げてElixirの恩恵を受けることができる 並行処理、マクロ、ビヘイビア、プロトコルは最後に学ぶ ここで紹介した以外はリスト操作に習熟してから学ぶ EVMの並行処理について 巷の良い話 巷の悪い話 概念自体の学習にコストが必要 (共有メモリ+スレッドより楽だけど)メッセージパッシングですら人類には早かった → 前提条件を変更する PhoenixでWebサービスを開発するなら、逐次処理を書くだけで、軽量プロセスの恩恵を享受できる →よってしばらく並行処理は忘れて良い、学習コストを払うのも後回しで良い 以下のように考えるとわかりやすい 「 Erlang で作られたWebサーバは一つの接続ごとに一つのプロセスを生成する。10万個のチャットのセッションが必要なら10万個のWebサーバが起動する。仮にその中の一つが落ちたとしても、誰も気にしない」 EVM=OSと似たプロセスの仕組みを持つ プロセスとは どういうことか EVMは各軽量プロセスにCPUを使う権限を与える 軽量プロセスが一定量処理すると別の軽量プロセスに権限が移る メリット 一つの軽量プロセスがCPUを占有しづらい (極論)軽量プロセスないで無限ループしてもシステム全体に影響が出にくい 軽量プロセス内はシンプルな逐次処理となる Elixirであれば同じ処理を簡単な逐次処理として記述できる →「どこでコンテキストが切り替わるのか」考慮不要 ネットワーク上の一意な住所を持つとは? EVMは各軽量プロセスに一意なアドレスを割り当てる PIDは複数のEVMで構成されたクラスタ上で一意 (利点) 軽量プロセス同士がメッセージを送受信し、共有メモリを使わずに連動できる 軽量プロセス同士がシグナルを送受信し監視や管理(再起動など)ができる (注意点) とはいえ、高頻度に再起動するとCPU負荷が上がるため注意 プロセスを使いまわすと、プロセス停止によるメモリ解放が行われないため、メモリリークしないように気を配る必要がある 速さはおまけ 軽量プロセスは耐障害性の向上するための仕組みであり、速さはおまけ => OSと似たことをしているのに早いわけがない Node.jsは他の動的言語と比較すると圧倒的に速い(V8というものが頑張っているらしい) 結果として、Elixir => 並行処理の学習コスト不要で、ほぼほぼに遅くないとても楽で安全な言語 巷の良い話 巷の悪い話 Elixirは純粋な関数型ではない そのため関数型由来の仕組みが少ない ○○由来の仕組みが少ない 複雑さが排除されていて、理解するための文脈が少ない 他者の記述したコードを読むのが楽 (注意)マクロ自体やマクロ由来の仕組みは複雑さを招きやすい 純粋だと言われる条件 参照透過な関数で表現できることが多い → ? e.g.
taku-exs.hatenablog.com
並列プログラミング言語 Elixir (エリクサー) におけるソフトウェアテスト〜基礎から最新展望まで - Qiita
(この記事は 「ソフトウェアテスト #2 Advent Calendar 2018」 の7日目です) 「ソフトウェアテスト #2 Advent Calendar 2018」6日目は @kokotatata さんの 「アジャイルもテスト自動化も当たり前?! ~AIがテスト設計をする日が来るかも~」 でした。 「ソフトウェアテスト #2 Advent Calendar 2018」7日目の今日は,残念ながら行けなかった JaSST'18 Kyushu にて事例発表を検討していたネタを披露しようと思います。最近「推し」の並列プログラミング言語 Elixir (エリクサー)におけるソフトウェアテストについてです。 なぜ Elixir に注目しているかというと,Elixir の持つ 並列処理性能と 耐障害性が高い上に, 文法が平易で記述が容易であるから,そして Phoenix(フェニックス)という 最速のウェブフレームワーク を持つからです。 まず Elixir における並列処理について説明しましょう。 1行目の1..1_000_000は,1から1,000,000までの要素からなるリストを生成します。なお,数字の間の_(アンダースコア)によって,数字を分割するコンマを表します。 2,3行目の先頭にある|>はパイプライン演算子で,パイプライン演算子の前に書かれている記述の値を,パイプライン演算子の後に書かれた関数の第1引数として渡します。すなわち,このような記述と等価です。Enum.map(Enum.map(1..1_000_000, foo), bar) 2,3行目に書かれている Enum.mapは,第1引数に渡されるリスト(など)の要素1つ1つに,第2引数で渡される関数を適用します。ここでは関数 foo を各要素に適用した後,関数 bar を各要素に適用します。もし,fooが2倍する関数で,barが1加える関数だった時には,これらの記述により,2倍してから1加える処理を1から1,000,000までの要素に適用したリスト,[3, 5, 7, ...]
qiita.com

性能面ではRailsを利用しているプロジェクトと比べて、サーバー台数を半分程度で抑えられているという事例もあるようです。
Elixirは構文がRubyに似てるところもありRubyistにとってはとっつきやすいかもしれません。
今回はdockerを使ってPhoenixのAPIサーバーを構築し、さらにElixir + Phoenix開発では必須と言っても良い便利なツールなどを紹介していければと思っています。
この記事がElixirとPhoenixに興味を持つきっかけになれば嬉しいです。
目次
対象読者
- RubyやRailsに少し飽きてきて他の言語やフレームワークにも触れてみたいと思っている人
- ElixirやPhoenixに興味はあったけど実際動かすまではやってない人
PR
UZUMAKIではアジャイル開発で新規事業の開発から、大規模Webアプリケーションのアーキテクチャ更新などの開発をしています。
お問い合わせはUZUMAKIのHPのお問合せフォームから
株式会社UZUMAKI
UZUMAKI(うずまき)はエンジニア・デザイナー・ディレクターで構成されるギルド型開発チーム。成長するWebサービスやアプリのリニューアル・リファクタリングに強みがあります。
uzumaki-inc.jp
本文
サーバー構築
まずはdockerを使いPhoenixサーバーが応答を返すところまで構築します。
(※ Docker for Mac 以外での動作確認はしていません。)
環境
MacBook Pro (Intel, 2020)
OS: macOS Monterey 12.6
Docker version 20.10.17
サーバー構成
言語: Elixir 1.14
フレームワーク: Phoenix 1.6.15
DB: mysql 8
作業ディレクトリ作成
$ mkdir todo-phoenix
$ cd todo-phoenixDockerfileとdocker-compose.ymlを用意
Dockerfile
FROM elixir:1.14-alpine
RUN apk update && apk add \
inotify-tools git build-base npm bash
RUN mix do local.hex --force, local.rebar --force, archive.install --force hex phx_new
WORKDIR /appdocker-compose.yml
version: '3'
services:
web:
build:
context: .
dockerfile: ./Dockerfile
tty: true
environment:
- MIX_ENV=dev
- PORT=4000
- DB_HOST=db
links:
- db
ports:
- 4000:4000
command: mix phx.server
volumes:
- .:/app
db:
image: mysql:8
environment:
- MYSQL_ROOT_PASSWORD=password
ports:
- 3306:3306
volumes:
- mysql_volume:/var/lib/mysql
volumes:
mysql_volume:build
$ docker-compose buildElixirとPhoenixのバージョンを確認。
# Elixir
$ docker compose run --rm web elixir -v
[+] Running 2/2
⠿ Volume "todo-phoenix_mysql_volume" Created 0.0s
⠿ Container todo-phoenix-db-1 Created 0.1s
[+] Running 1/1
⠿ Container todo-phoenix-db-1 Started 0.4s
Erlang/OTP 25 [erts-13.0.4] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [jit]
Elixir 1.14.0 (compiled with Erlang/OTP 25)
# Phoenix
docker compose run --rm web mix phx.new -v
[+] Running 1/0
⠿ Container todo-phoenix-db-1 Running 0.0s
Phoenix installer v1.6.15Phoenixプロジェクト作成
$ docker-compose run web mix phx.new . --app todo_phoenix --database mysql --no-assets --no-html --no-gettext
# The directory /app already exists. Are you sure you want to continue? [Yn] Y
# Fetch and install dependencies? [Yn] Yphx.newのoptionsはこちらを参照してください。
プロジェクト作成後のディレクトリ構造は以下のようになっています。
├── Dockerfile
├── README.md
├── _build # 階層が深いので省略。ビルドしたファイルを格納
├── config
│ ├── config.exs
│ ├── dev.exs
│ ├── prod.exs
│ ├── runtime.exs
│ └── test.exs
├── deps # 階層が深いので省略。ライブラリファイルを格納
├── docker-compose.yml
├── lib
│ ├── todo_phoenix
│ │ ├── application.ex
│ │ ├── mailer.ex
│ │ └── repo.ex
│ ├── todo_phoenix.ex
│ ├── todo_phoenix_web
│ │ ├── controllers
│ │ ├── endpoint.ex
│ │ ├── router.ex
│ │ ├── telemetry.ex
│ │ └── views
│ │ ├── error_helpers.ex
│ │ └── error_view.ex
│ └── todo_phoenix_web.ex
├── mix.exs
├── mix.lock
├── priv
│ └── repo
│ ├── migrations
│ └── seeds.exs
└── test
├── support
│ ├── conn_case.ex
│ └── data_case.ex
├── test_helper.exs
└── todo_phoenix_web
├── controllers
└── views
└── error_view_test.exsディレクトリ構造に関してはこちらに詳しく書かれています。
設定変更
config/dev.exsの以下を修正します。
@@ -3,8 +3,8 @@
# Configure your database
config :todo_phoenix, TodoPhoenix.Repo,
username: "root",
- password: "",
- hostname: "localhost",
+ password: "password", # dockerの設定情報に合わせてDBの接続情報を編集
+ hostname: "db", # dockerの設定情報に合わせてDBの接続情報を編集
database: "todo_phoenix_dev",
stacktrace: true,
show_sensitive_data_on_connection_error: true,
@@ -19,7 +19,7 @@
config :todo_phoenix, TodoPhoenixWeb.Endpoint,
# Binding to loopback ipv4 address prevents access from other machines.
# Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
- http: [ip: {127, 0, 0, 1}, port: 4000],
+ http: [ip: {0, 0, 0, 0}, port: 4000], # コンテナの外からアクセスできるようにする
check_origin: false,
code_reloader: true,
debug_errors: true,DB作成
$ docker-compose run --rm web mix ecto.create
Creating todo-phoenix_web_run ... done
The database for TodoPhoenix.Repo has been created動作確認
Phoenix Serverから200のHTTPステータスが返ることを確認します。
# コンテナを起動
$ docker-compose up
# 別窓でアクセス確認
$ curl -I http://localhost:4000/dashboard/home
HTTP/1.1 200 OKAPI作成
ここからは実際に簡単なAPIを作成し動かすところまでやっていきます。
今回はtodoアプリケーションのtask追加と取得ができるようにします。
スキーマ作成
以下の情報でスキーマを作成していきます。
テーブル名: todo_tasks
カラム: content, state
$ docker-compose run --rm web mix phx.gen.schema Todo.Task todo_tasks content:string state:integer
Creating todo-phoenix_web_run ... done
* creating lib/todo_phoenix/todo/task.ex
* creating priv/repo/migrations/20221030080244_create_todo_tasks.exsコマンドを実行するとスキーマファイルとマイグレーションファイルの2ファイルが作成されます。
スキーマファイルを開いてみます。
lib/todo_phoenix/todo/task.ex
defmodule TodoPhoenix.Todo.Task do
use Ecto.Schema
import Ecto.Changeset
schema "todo_tasks" do
field :content, :string
field :state, :integer
timestamps()
end
@doc false
def changeset(task, attrs) do
task
|> cast(attrs, [:content, :state])
|> validate_required([:content, :state])
end
endカラムの型や簡単なバリデーションが定義されていることが分かります。
マイグレーション実行
$ docker-compose run --rm web mix ecto.migrate
Creating todo-phoenix_web_run ... done
Compiling 1 file (.ex)
Generated todo_phoenix app
08:07:15.936 [info] == Running 20221030080244 TodoPhoenix.Repo.Migrations.CreateTodoTasks.change/0 forward
08:07:16.000 [info] create table todo_tasks
08:07:16.050 [info] == Migrated 20221030080244 in 0.0smysqlに接続し、テーブルを確認してみます。
$ mysql -h 127.0.0.1 -P 3306 -u root -p todo_phoenix_dev
mysql> desc todo_tasks;
+-------------+-----------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+-----------------+------+-----+---------+----------------+
| id | bigint unsigned | NO | PRI | NULL | auto_increment |
| content | varchar(255) | YES | | NULL | |
| state | int | YES | | NULL | |
| inserted_at | datetime | NO | | NULL | |
| updated_at | datetime | NO | | NULL | |
+-------------+-----------------+------+-----+---------+----------------+
5 rows in set (0.01 sec)テーブルが作成されていますね!
ではいよいよコーディングをしていきましょう。
コンテキスト作成
コンテキストとは関連する関数を纏める専用モジュールです。詳しくはこちらを参照してください。
ちなみに先に出たスキーマは、リレーション定義も書くのでRailsのモデルに似ていますが、ビジネスロジックに関してはスキーマではなくコンテキストに書くことが多いです。
コンテキストを作成する前にlib下のディレクトリ構造を見てみます。
(Phoenixでは主にlib下をさわります。
lib
├── todo_phoenix
│ ├── application.ex
│ ├── mailer.ex
│ ├── repo.ex
│ └── todo
│ └── task.ex
├── todo_phoenix.ex
├── todo_phoenix_web
│ ├── controllers
│ ├── endpoint.ex
│ ├── router.ex
│ ├── telemetry.ex
│ └── views
│ ├── error_helpers.ex
│ └── error_view.ex
└── todo_phoenix_web.ex
5 directories, 11 filesアプリケーション名(todo_phoenix)の下には先ほど確認したスキーマ定義ファイル(task.ex)がありますね。
また、controllerやviewはtodo_phoenix_web下に配置する構造になっています。
今回コンテキストはtodo_phoenix下にtodo.exという名前で作成してみます。
lib/todo_phoenix/todo.ex
defmodule TodoPhoenix.Todo do
import Ecto.Query, warn: false
alias TodoPhoenix.Repo
alias TodoPhoenix.Todo.Task
@doc """
タスク一覧を取得する。
"""
def list_tasks do
Repo.all(Task)
end
@doc """
タスクを作成する。
"""
def create_task(attrs \\ %{}) do
%Task{}
|> Task.changeset(attrs)
|> Repo.insert()
end
endコンソールから動作を確認してみます。
iexコンソールへの接続
# コンテナに接続
$ docker exec -it todo-phoenix_web_1 bash
$ iex -S mix
Erlang/OTP 25 [erts-13.0.4] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:1] [jit]
Compiling 1 file (.ex)
Generated todo_phoenix app
Interactive Elixir (1.14.0) - press Ctrl+C to exit (type h() ENTER for help)iexコンソール
# コンテキストに定義した関数を使いtaskを取得
iex(1)> TodoPhoenix.Todo.list_tasks
[debug] QUERY OK source="todo_tasks" db=0.5ms decode=12.6ms queue=17.3ms idle=1085.5ms
SELECT t0.`id`, t0.`content`, t0.`state`, t0.`inserted_at`, t0.`updated_at` FROM `todo_tasks` AS t0 []
↳ anonymous fn/4 in :elixir.eval_external_handler/1, at: src/elixir.erl:298
[]
# 登録が無いので空の配列を返す
# コンテキストに定義した関数を使いtaskを作成
iex(2)> TodoPhoenix.Todo.create_task(%{content: "hoge", state: 0})
[debug] QUERY OK db=4.0ms queue=1.0ms idle=1991.9ms
INSERT INTO `todo_tasks` (`content`,`state`,`inserted_at`,`updated_at`) VALUES (?,?,?,?) ["hoge", 0, ~N[2022-10-30 10:27:00], ~N[2022-10-30 10:27:00]]
↳ anonymous fn/4 in :elixir.eval_external_handler/1, at: src/elixir.erl:298
{:ok,
%TodoPhoenix.Todo.Task{
__meta__: #Ecto.Schema.Metadata<:loaded, "todo_tasks">,
id: 1,
content: "hoge",
state: 0,
inserted_at: ~N[2022-10-30 10:27:00],
updated_at: ~N[2022-10-30 10:27:00]
}}
iex(3)> TodoPhoenix.Todo.list_tasks
[debug] QUERY OK source="todo_tasks" db=2.9ms queue=0.1ms idle=1543.0ms
SELECT t0.`id`, t0.`content`, t0.`state`, t0.`inserted_at`, t0.`updated_at` FROM `todo_tasks` AS t0 []
↳ anonymous fn/4 in :elixir.eval_external_handler/1, at: src/elixir.erl:298
[
%TodoPhoenix.Todo.Task{
__meta__: #Ecto.Schema.Metadata<:loaded, "todo_tasks">,
id: 1,
content: "hoge",
state: 0,
inserted_at: ~N[2022-10-30 10:27:00],
updated_at: ~N[2022-10-30 10:27:00]
}
]
# 正常に登録されているついでにtask.stateをEnumの管理に変更してみます。
lib/todo_phoenix/todo/task.ex
# 修正前
field :state, :integer
# 修正後
field :state, Ecto.Enum, values: [new: 0, doing: 1, done: 2], default: :new再度コンソールから実行してみましょう。
# 既にコンソールを開いてる場合は以下で再コンパイルをお願いします。
iex(4)> recompile
# stateはアトム型で指定できる
iex(5)> TodoPhoenix.Todo.create_task(%{content: "fuga", state: :doing})
[debug] QUERY OK db=5.4ms queue=1.2ms idle=1078.8ms
INSERT INTO `todo_tasks` (`content`,`state`,`inserted_at`,`updated_at`) VALUES (?,?,?,?) ["fuga", :doing, ~N[2022-10-30 11:04:25], ~N[2022-10-30 11:04:25]]
↳ anonymous fn/4 in :elixir.eval_external_handler/1, at: src/elixir.erl:298
{:ok,
%TodoPhoenix.Todo.Task{
__meta__: #Ecto.Schema.Metadata<:loaded, "todo_tasks">,
id: 2,
content: "fuga",
state: :doing,
inserted_at: ~N[2022-10-30 11:04:25],
updated_at: ~N[2022-10-30 11:04:25]
}}
# stateを指定しない場合デフォルト値のnewが入る
iex(6)> TodoPhoenix.Todo.create_task(%{content: "piyo"})
[debug] QUERY OK db=8.4ms queue=1.8ms idle=1398.0ms
INSERT INTO `todo_tasks` (`content`,`state`,`inserted_at`,`updated_at`) VALUES (?,?,?,?) ["piyo", :new, ~N[2022-10-30 11:04:32], ~N[2022-10-30 11:04:32]]
↳ anonymous fn/4 in :elixir.eval_external_handler/1, at: src/elixir.erl:298
{:ok,
%TodoPhoenix.Todo.Task{
__meta__: #Ecto.Schema.Metadata<:loaded, "todo_tasks">,
id: 3,
content: "piyo",
state: :new,
inserted_at: ~N[2022-10-30 11:04:32],
updated_at: ~N[2022-10-30 11:04:32]
}}railsでもEnumを使うことが多いですがPhoenixでも同じように使えますね。
さて、コンソールで確認した限りコンテキストの動作に問題は無さそうです。
次はコントローラ、ビューを作成していきます。
コントローラ、ビュー作成
lib/todo_phoenix_web/controllers/task_controller.ex
defmodule TodoPhoenixWeb.TaskController do
use TodoPhoenixWeb, :controller
alias TodoPhoenix.Todo
alias TodoPhoenix.Todo.Task
def index(conn, _params) do
tasks = Todo.list_tasks()
render(conn, "index.json", tasks: tasks)
end
def create(conn, %{"task" => task_params}) do
case Todo.create_task(task_params) do
{:ok, %Task{}} ->
send_resp(conn, :created, "")
{:error, _} ->
send_resp(conn, :unprocessable_entity, "")
end
end
def create(conn, _), do: send_resp(conn, :bad_request, "")
endlib/todo_phoenix_web/views/task_view.ex
defmodule TodoPhoenixWeb.TaskView do
use TodoPhoenixWeb, :view
alias TodoPhoenixWeb.TaskView
def render("index.json", %{tasks: tasks}) do
%{data: render_many(tasks, TaskView, "task.json")}
end
def render("task.json", %{task: task}) do
%{
id: task.id,
content: task.content,
state: task.state
}
end
endlib/todo_phoenix_web/router.ex
scope "/api", TodoPhoenixWeb do
pipe_through :api
resources "/tasks", TaskController, only: [:create, :index] # 追加
endAPI動作確認
動作確認する前に、分かり易いようにコンテキストのテスト時に作成したtodo_tasksのデータを予め削除しておきましょう。
また、コンテナが起動してない場合はdocker-compose upで起動してください。
# GET tasks
curl http://localhost:4000/api/tasks | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 11 100 11 0 0 53 0 --:--:-- --:--:-- --:--:-- 56
{
"data": []
}
# データは空
# POST tasks
$ curl -v http://localhost:4000/api/tasks -H "Content-Type: application/json" -d '{"task":{"content": "foo"}}'
* Trying 127.0.0.1:4000...
* Connected to localhost (127.0.0.1) port 4000 (#0)
> POST /api/tasks HTTP/1.1
> Host: localhost:4000
> User-Agent: curl/7.79.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 27
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 201 Created
# GET tasks
curl http://localhost:4000/api/tasks | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 50 100 50 0 0 277 0 --:--:-- --:--:-- --:--:-- 294
{
"data": [
{
"content": "foo",
"id": 4,
"state": "new"
}
]
}
# データが作成されている
# POST tasks
curl -v http://localhost:4000/api/tasks -H "Content-Type: application/json" -d '{"task":{"content": "bar"}}'
* Trying 127.0.0.1:4000...
* Connected to localhost (127.0.0.1) port 4000 (#0)
> POST /api/tasks HTTP/1.1
> Host: localhost:4000
> User-Agent: curl/7.79.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 27
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 201 Created
# GET tasks
curl http://localhost:4000/api/tasks | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 90 100 90 0 0 511 0 --:--:-- --:--:-- --:--:-- 545
{
"data": [
{
"content": "foo",
"id": 4,
"state": "new"
},
{
"content": "bar",
"id": 5,
"state": "new"
}
]
}
# 追加で1件データが作成されている良さそうですね!
次はテストを書いていきます。
テスト
設定変更
config/test.exsの以下を修正します。
@@ -7,8 +7,8 @@
# Run `mix help test` for more information.
config :todo_phoenix, TodoPhoenix.Repo,
username: "root",
- password: "",
- hostname: "localhost",
+ password: "password", # dockerの設定情報に合わせてDBの接続情報を編集
+ hostname: "db", # dockerの設定情報に合わせてDBの接続情報を編集
database: "todo_phoenix_test#{System.get_env("MIX_TEST_PARTITION")}",
pool: Ecto.Adapters.SQL.Sandbox,
pool_size: 10fixturesを準備
$ mkdir test/support/fixturestest/support/fixtures/todo_fixtures.ex
defmodule TodoPhoenix.TodoFixtures do
def task_fixture(attrs \\ %{}) do
{:ok, task} =
attrs
|> Enum.into(%{
content: "hoge",
state: :new
})
|> TodoPhoenix.Todo.create_task()
task
end
endコンテキストテスト作成
$ mkdir test/todo_phoenixtest/todo_phoenix/todo_test.exs
defmodule TodoPhoenix.TodoTest do
use TodoPhoenix.DataCase
import TodoPhoenix.TodoFixtures
alias TodoPhoenix.Todo
alias TodoPhoenix.Todo.Task
describe "task.list_tasks" do
test "taskが配列で取得できること" do
task = task_fixture()
assert Todo.list_tasks() == [task]
end
end
describe "task.create_task" do
@valid_attrs %{content: "bar", state: :doing}
@invalid_attrs %{content: nil}
test "taskが登録できること" do
assert {:ok, %Task{} = task} = Todo.create_task(@valid_attrs)
assert task.content == "bar"
assert task.state == :doing
end
test "パラメータが不正な場合、Changesetエラーが返却されること" do
assert {:error, %Ecto.Changeset{}} = Todo.create_task(@invalid_attrs)
end
end
endテスト実行
# コンテナ内で実行しています
# 初回はコンパイルが走ってテスト実行まで時間がかかります。
$ MIX_ENV=test mix test test/todo_phoenix/todo_test.exs
...
Finished in 0.4 seconds (0.00s async, 0.4s sync)
3 tests, 0 failuresコントローラテスト作成
test/todo_phoenix_web/controllers/task_controller_test.exs
defmodule TodoPhoenixWeb.TaskControllerTest do
use TodoPhoenixWeb.ConnCase
@create_attrs %{
content: "bar",
state: :new
}
@invalid_attrs %{content: nil}
@blank_params %{}
setup %{conn: conn} do
{:ok, conn: put_req_header(conn, "accept", "application/json")}
end
describe "index" do
test "ステータスコード200を返却すること", %{conn: conn} do
conn = get(conn, Routes.task_path(conn, :index))
assert json_response(conn, :ok)["data"] == []
end
end
describe "create task" do
test "登録に成功した場合、ステータスコード201を返却すること", %{conn: conn} do
conn = post(conn, Routes.task_path(conn, :create), task: @create_attrs)
assert response(conn, :created)
end
test "パラメータが不正な場合、ステータスコード422を返却すること", %{conn: conn} do
conn = post(conn, Routes.task_path(conn, :create), task: @invalid_attrs)
assert response(conn, :unprocessable_entity)
end
test "パラメータが空の場合、ステータスコード400を返すこと", %{conn: conn} do
conn = post(conn, Routes.task_path(conn, :create), @blank_params)
assert response(conn, :bad_request)
end
end
endテスト実行
$ MIX_ENV=test mix test test/todo_phoenix_web/controllers/task_controller_test.exs
....
Finished in 0.7 seconds (0.00s async, 0.7s sync)
4 tests, 0 failures全てのテストを実行
$ MIX_ENV=test mix test
.........
Finished in 0.9 seconds (0.4s async, 0.4s sync)
9 tests, 0 failures
# 内2つはデフォルトで実装されているテストです。ここまででAPIサーバーの構築、テストの実装まで行いました。
次はElixir + Phoenix開発では必須のライブラリ(hex)を導入してみます。
Credo
credoはrailsではお馴染みのrubocopのような構文チェックツールです。
Overview - Credo v1.6.7
Credo is a static code analysis tool for the Elixir language with a focus on teaching and code consistency. credo can show you refactoring opportunities in your code, complex code fragments, warn you about common mistakes, show inconsistencies in your naming scheme and - if needed - help you enforce a desired coding style.
hexdocs.pm
では導入していきます。
mix.exs
defp deps do
[
{:phoenix, "~> 1.6.15"},
{:phoenix_ecto, "~> 4.4"},
{:ecto_sql, "~> 3.6"},
{:myxql, ">= 0.0.0"},
{:phoenix_live_dashboard, "~> 0.6"},
{:swoosh, "~> 1.3"},
{:telemetry_metrics, "~> 0.6"},
{:telemetry_poller, "~> 1.0"},
{:jason, "~> 1.2"},
{:plug_cowboy, "~> 2.5"}, # カンマを追加
{:credo, "~> 1.6", only: [:dev, :test], runtime: false}, # 追加
]
endインストール
$ docker-compose run --rm web mix deps.get実行する前に、警告を出してくれることを期待してコード中にデバッグ文を入れてみます。
lib/todo_phoenix_web/controllers/task_controller.ex
12 def create(conn, %{"task" => task_params}) do
13 IO.inspect(task_params) # こちらを追加実行
$ docker-compose run --rm web mix credo結果
先ほど入れたデバッグ文は[W]でちゃんと警告してくれてますね。
Dialyzer
Elixirはrubyと同じく動的型付け言語ですが、Typespecsという型と仕様を宣言するための記法を提供しており、dialyzerはそちらをチェクしてくれるツールです。
Dialyxir
Mix tasks to simplify use of Dialyzer in Elixir projects. Elixir 1.6 is required, to support the new pretty printing feature. If your project is not yet on 1.6, continue to specify 0.5 in your mix deps. Warning messages have been greatly improved, but are filtered through the legacy formatter to support your existing ignore files.
hexdocs.pm
では導入していきます。
mix.exs
defp deps do
[
{:phoenix, "~> 1.6.15"},
{:phoenix_ecto, "~> 4.4"},
{:ecto_sql, "~> 3.6"},
{:myxql, ">= 0.0.0"},
{:phoenix_live_dashboard, "~> 0.6"},
{:swoosh, "~> 1.3"},
{:telemetry_metrics, "~> 0.6"},
{:telemetry_poller, "~> 1.0"},
{:jason, "~> 1.2"},
{:plug_cowboy, "~> 2.5"},
{:credo, "~> 1.6", only: [:dev, :test], runtime: false},
{:dialyxir, "~> 1.0", only: [:dev], runtime: false}, # 追加
]
endインストール
$ docker-compose run --rm web mix deps.get型を定義
lib/todo_phoenix/todo/task.ex
defmodule TodoPhoenix.Todo.Task do
use Ecto.Schema
import Ecto.Changeset
@type t :: %__MODULE__{} # 追加。これを追加することでmodule名.t()で他から参照できるようになります。
schema "todo_tasks" dolib/todo_phoenix/todo.ex
defmodule TodoPhoenix.Todo do
import Ecto.Query, warn: false
alias TodoPhoenix.Repo
alias TodoPhoenix.Todo.Task
@doc """
タスク一覧を取得する。
"""
@spec list_tasks() :: [Task.t()] # 追加
def list_tasks do
Repo.all(Task)
end
@doc """
タスクを作成する。
"""
@spec create_task(map()) :: {:ok, Task.t()} # 追加
def create_task(attrs \\ %{}) do
%Task{}
|> Task.changeset(attrs)
|> Repo.insert()
end
end実行
$ docker-compose run --rm web mix dialyzer結果
エラーになりましたね。エラー内容をみると、コントローラの16行目でerrorのパターンマッチの記載が有るが、コンテキスト側の返却型定義にerrorパターンが無いと警告されていることが分かります。
lib/todo_phoenix_web/controllers/task_controller.ex
13 case Todo.create_task(task_params) do
14 {:ok, %Task{}} ->
15 send_resp(conn, :created, "")
16 {:error, _} ->
17 send_resp(conn, :unprocessable_entity, "")
18 endコンテキストのcreate_task関数のspecを以下のように複数の返却型があるように修正してみましょう。
この関数はRepo.insert()の結果を返してるのでそちらを参考にエラーパターンを追加します。
lib/todo_phoenix/todo.ex
@doc """
タスクを作成する。
"""
@spec create_task(map()) :: {:ok, Task.t()} | {:error, Ecto.Changeset.t()} # エラーパターンを追加
def create_task(attrs \\ %{}) do
%Task{}
|> Task.changeset(attrs)
|> Repo.insert()
end再度実行してみます。
$ docker-compose run --rm web mix dialyzer
Creating todo-phoenix_web_run ... done
Compiling 1 file (.ex)
Finding suitable PLTs
Checking PLT...
[:asn1, :castore, :compiler, :connection, :cowboy, :cowboy_telemetry, :cowlib, :crypto, :db_connection, :decimal, :ecto, :ecto_sql, :eex, :elixir, :jason, :kernel, :logger, :mime, :myxql, :phoenix, :phoenix_ecto, :phoenix_html, :phoenix_live_dashboard, :phoenix_live_view, :phoenix_pubsub, :phoenix_template, :phoenix_view, :plug, :plug_cowboy, :plug_crypto, :public_key, :ranch, :runtime_tools, :ssl, :stdlib, :swoosh, :telemetry, :telemetry_metrics, :telemetry_poller, :xmerl]
PLT is up to date!
No :ignore_warnings opt specified in mix.exs and default does not exist.
Starting Dialyzer
[
check_plt: false,
init_plt: '/app/_build/dev/dialyxir_erlang-25.0.4_elixir-1.14.0_deps-dev.plt',
files: ['/app/_build/dev/lib/todo_phoenix/ebin/Elixir.TodoPhoenix.Application.beam',
'/app/_build/dev/lib/todo_phoenix/ebin/Elixir.TodoPhoenix.Mailer.beam',
'/app/_build/dev/lib/todo_phoenix/ebin/Elixir.TodoPhoenix.Repo.beam',
'/app/_build/dev/lib/todo_phoenix/ebin/Elixir.TodoPhoenix.Todo.Task.beam',
'/app/_build/dev/lib/todo_phoenix/ebin/Elixir.TodoPhoenix.Todo.beam', ...],
warnings: [:unknown]
]
Total errors: 0, Skipped: 0, Unnecessary Skips: 0
done in 0m3.12s
done (passed successfully)警告無く完了しましたね。
このようにtypespec + dialyzer を使用することで、型安全で可読性の高いコードを書いていけそうです。
まとめ
ElixirとPhoenixを使ってサクッとAPIサーバーを作成することができました。
静的解析ツールのcredoやdialyzerはCIで設定すれば便利そうですね。
Elixirは今年仕事で少し書く機会がありましたが、パイプライン演算子やパターンマッチなど、凄く魅力的で書いてて楽しい言語です。
最近話題になっているリアルタイムUIが作れるLiveViewも面白そうですね。
やってみた系の記事では以下が詳しく書かれてそうです。
【感謝記念】Phoenix 1.4でElixirしか書いてないが React的SPA/リアルタイムUIが作れる「LiveView」(APIとJavaScriptは書いていない) - Qiita
下記Elixirコミュニティ運営/所属の piacere です、ご覧いただいてありがとございます Elixir実装の芽を愛でるコミュニティ「Elixir Digitalization Implementors」 福岡Elixirコミュニティ「fukuoka.ex」 小倉Elixirコミュニティ「kokura.ex」 ...
qiita.com

そして2022年11月現在、WEB+DB PRESS最新号の Vol.131では、何とRustと並んで表紙にElixirが載っています。
WEB+DB PRESS Vol.131
特集1では,Rust習得の一歩を踏み出してみます。特集2では,パターンマッチをサポートする関数型言語Elixirを取り上げます。特集3ではHTTP/3サーバを実装することで,HTTP/3がどのようなしくみで動作しているかを学びます。
gihyo.jp
ElixirとPhoenixは今後ますます盛り上がってくるのではないかと思っています。
参考リンク
- Phoenix v1.6 hexdocs 日本語訳
- 【メモ】私が愛する Elixir/Erlang の楽しさと辛さ
- 並列プログラミング言語 Elixir (エリクサー) におけるソフトウェアテスト〜基礎から最新展望まで
- 『ロマサガRS』は如何にリリース直後のアクセスラッシュを乗り切ったのか…クイックスケールの要点やElixirを活用した技術を解説
- 様々な開発マシンでもPhoenixが動くDockerfile/docker-composeの作り方
- docker composeでphoenixアプリを立ち上げるまで
- Elixir+Phoenix - running a dev setup inside docker
- Phoenix 1.3のディレクトリ構造とContext
- ElixirでDialyzerを用いた静的解析を行い、型(Typespec)を最大限に活用する
- 【感謝記念】Phoenix 1.4でElixirしか書いてないが React的SPA/リアルタイムUIが作れる「LiveView」(APIとJavaScriptは書いていない)
PR
XではUZUMAKIの新しい働き方や日常の様子を紹介!ぜひフォローをお願いします!
noteではUZUMAKIのメンバー・クライアントインタビュー、福利厚生を紹介!
UZUMAKI|note
株式会社UZUMAKIです。主に代表の工藤が投稿してますが、ほかのメンバーもたまに投稿します。https://uzumaki-inc.jp/ 受託開発を生業としてますが、リモートワークを通じて、今までにない組織形態での働き方を模索、実践しています。
note.com
UZUMAKIではRailsエンジニアを絶賛募集中です。
↓の記事を読んでご興味を持っていただいた方は、ぜひ応募よろしくお願いします!
UZUMAKI 採用情報
これを実践するには、通常の社会で求められる上司による部下の管理が必要という前提が不要になってきます。例えば、時間管理をしない、圧倒的な性善説で動く、メンバーは自発的に動くということをUZUMAKIでは当たり前のように行っております。 現在は、 コーポレートサイト記載の企業 ...
job.uzumaki-inc.jp

是非応募宜しくおねがいします!