Elixir の Plug を使って HTTP サーバのモックを作る覚書です。
ドキュメントに詳しく書かれていますので、詳しい話はこちらを参照してください。
プロジェクトを用意する
新しいプロジェクトを作ります。
起動時に自動で Plug のアプリケーションを起動したいので --sup
オプションを指定してアプリケーションの雛形を生成しておきます。
$ mix new mock_server --sup
$ cd mock_server
mix.exs
を編集して依存する cowboy
と plug
の記述を追加します。詳細は、公開されたパッケージを管理している Hex で確認できます。
この他にもコマンドラインから mix hex.info
コマンドを使って調べることもできます。
$ mix hex.info cowboy
Small, fast, modular HTTP server.
Config: {:cowboy, "~> 2.4"}
Releases: 2.4.0, 2.3.0, 2.2.2, 2.2.1, 2.2.0, 2.1.0, 2.0.0, 1.1.2, ...
Licenses: ISC
Links:
GitHub: https://github.com/ninenines/cowboy
$ mix hex.info plug
A specification and conveniences for composable modules between web applications
Config: {:plug, "~> 1.6"}
Releases: 1.6.2, 1.6.1, 1.6.0, 1.5.1, 1.5.0, 1.5.0-rc.2, 1.5.0-rc.1, 1.5.0-rc.0, ...
Licenses: Apache 2
Links:
GitHub: https://github.com/elixir-plug/plug
ここでは表示された Config:
の内容そのままに mix.exs
に記述します。
defp deps do
[
{:cowboy, "~> 2.4"},
{:plug, "~> 1.6"}
]
end
依存するパッケージの取得とコンパイル。
$ mix do deps.get, deps.compile
ハンドラを書く
Plug の説明に倣ってハンドラを書きます。
何がリクエストされてもステータスコード 200 で Hello world
を返すコードです。
defmodule MockServer do
import Plug.Conn
def init(options) do
options
end
def call(conn, ) do
conn
|> send_resp(200, "Hello world\n")
end
end
lib/mock_server/
に作成されているアプリケーションの雛形を編集して、 起動時に Plug のアプリケーションを起動するようにします。
defmodule MockServer.Application do
@moduledoc false
use Application
def start(, ) do
children = [
Plug.Adapters.Cowboy2.child_spec(scheme: :http, plug: MockServer, options: [port: 4000])
]
opts = [strategy: :one_for_one, name: MockServer.Supervisor]
Supervisor.start_link(children, opts)
end
end
サーバを起動する
mix run
コマンドでアプリケーションを起動します。コマンドはアプリケーションを起動するだけなので、そのままではアプリケーションが起動したらコマンド自体は終了してしまい一緒にアプリケーションも終了してしまいます。
アプリケーションが起動したあとコマンドが終了しないように --no-halt
オプションを指定して実行します。
$ mix run --no-halt
Compiling 2 files (.ex)
最初の実行のときに表示されるメッセージから編集した二つのファイルがコンパイルされたことがわかります。
別のコンソールからリクエストを送ってみます。
$ curl http://localhost:4000
Hello world
応答が返りました。
Ctrl+C で終了します。
リクエストの状況を知る
どのようなリクエストを受けているか知りたいので IO.inspect/1
を挟んでみます。
MockServer.call/2
を編集して、パイプラインの途中に IO.inspect/1
を追加します。
def call(conn, ) do
conn
|> IO.inspect()
|> send_resp(200, "Hello world (#{next_count()})\n")
end
実行。
$ mix run --no-halt
Compiling 1 file (.ex)
同じように別のコンソールからリクエストを送ると Plug.Conn
構造体の内容が表示されます。
%Plug.Conn{
adapter: {Plug.Adapters.Cowboy2.Conn, :...},
assigns: %{},
before_send: [],
body_params: %Plug.Conn.Unfetched{aspect: :body_params},
cookies: %Plug.Conn.Unfetched{aspect: :cookies},
halted: false,
host: "localhost",
method: "GET",
owner:
params: %Plug.Conn.Unfetched{aspect: :params},
path_info: [],
path_params: %{},
peer: {{127, 0, 0, 1}, 61667},
port: 4000,
private: %{},
query_params: %Plug.Conn.Unfetched{aspect: :query_params},
query_string: "",
remote_ip: {127, 0, 0, 1},
req_cookies: %Plug.Conn.Unfetched{aspect: :cookies},
req_headers: [
{"accept", "*/*"},
{"host", "localhost:4000"},
{"user-agent", "curl/7.54.0"}
],
request_path: "/",
resp_body: nil,
resp_cookies: %{},
resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"}],
scheme: :http,
script_name: [],
secret_key_base: nil,
state: :unset,
status: nil
}
バックエンドで実行する
--detached
オプションを指定して elixir
コマンドを利用して起動すると、コンソールから切り離して実行することができます。
オプションの内容はコマンドのヘルプで確認できます。
$ elixir --help
Usage: elixir [options] [.exs file] [data]
-e COMMAND Evaluates the given command (*)
...
--detached Starts the Erlang VM detached from console
...
実行。コマンドの実行後すぐにプロンプトが表示されますが、リクエストを送るとアプリケーションが起動していることがわかります。
$ elixir --detached -S mix run --no-halt
$ curl http://localhost:4000
Hello world
コンソールから切り離されているので、先ほどは表示されていた構造体の内容が今回は表示されません。
Logger を利用してログをファイルに出力する記事を書いていますのでこちらも参照してみてください。
また logger_file_backend などのパッケージが HEX に登録されていますのでこれらを利用できます。
終了は kill コマンドなどを駆使してください。
リモートシェルで状態を知る
remote shell を使うと実行しているノードに接続して状態を確認したり操作したりすることができます。
下準備として、状態を持つようにします。
エージェントとして振る舞う Counter モジュールを追加します。
defmodule MockServer.Counter do
use Agent
@name __MODULE__
def start_link() do
Agent.start_link(fn -> 1 end, name: @name)
end
def next_count do
Agent.get_and_update(@name, &{&1, &1 + 1})
end
end
MockServer.Counter.start_link/1
で起動すると MockServer.Counter.next_count/0
を呼ぶごとに 1 ずつ大きくなる値を返します。
MockServer.Application.start/2
を編集して Counter モジュールも自動的に起動するようにします。
children = [
Plug.Adapters.Cowboy2.child_spec(scheme: :http, plug: DummyServer, options: [port: 4000]),
MockServer.Counter
]
MockServer.call/2
を編集し MockServer.Counter.next_count/0
を利用してリクエストを受けた回数をレスポンスに含めるようにします。
import MockServer.Counter
def call(conn, ) do
conn
|> IO.inspect()
|> send_resp(200, "Hello world (#{next_count()})\n")
end
ノードに名前をつけて起動します。
$ elixir --sname foo -S mix run --no-halt
まず、状態を持つようになったことを確認します。
別のコンソールからリクエストを送ると、リクエストを送るたびに数が増えていくのがわかります。
$ curl http://localhost:4000
Hello world (1)
$ curl http://localhost:4000
Hello world (2)
$ curl http://localhost:4000
Hello world (3)
次に別のコンソールから、こちらも名前をつけて iex
を起動します。
$ iex --sname bar
iex(bar@emattsan)1>
プロンプトに「コマンドラインで指定した名前 + @
+ ホスト名」がノード名として表示されているのがわかります。
ここから foo に接続します。
まず Ctrl+G を押します。user switch command のプロンプトが表示されます。
iex(bar@emattsan)1>
User switch command
-->
?
を入力すると利用できるコマンドが表示されます。
User switch command
--> ?
c [nn] - connect to job
i [nn] - interrupt job
k [nn] - kill job
j - list all jobs
s [shell] - start local shell
r [node [shell]] - start remote shell
q - quit erlang
? | h - this message
-->
リモートシェルを起動するには r
を利用します。
接続先のノード foo をホスト名付きで指定します。シェルには Elixir.IEx
を指定します。
ノード名とシェル名はアトムで指定する必要がありますが、この user switch command は Erlang の文脈で動いているので @
を含む文字列や大文字から始まる文字列がアトムとして扱われるようにシングルクォートで囲みます。
またデフォルトでは Erlang のシェルが起動するので IEx
を明示的に指定します。
--> r 'foo@emattsan' 'Elixir.IEx'
-->
j
で状態を確認します。1 が最初に起動したシェル、 2 が今回指定したシェルです。 *
が表示されているので 2 がデフォルトになっているのがわかります。
--> j
1 {erlang,apply,[#Fun<Elixir.IEx.CLI.1.111201631>,[]]}
2* {'foo@emattsan','Elixir.IEx',start,[]}
-->
c
でノードに接続します。番号を指定していないのでデフォルトが選択されます。
--> c
Interactive Elixir (1.7.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(foo@emattsan)1>
プロンプトの表示が foo に代わっていることがわかります。
…と、長々と iex を起動してからリモートシェルで接続する手順を書きましたが、--remsh
オプションを使うことで起動時に接続先を指定することができます。
$ iex --sname bar --remsh foo@emattsan
Erlang/OTP 21 [erts-10.0.5] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe] [dtrace]
Interactive Elixir (1.7.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(foo@emattsan)1>
先ほどリクエストを送って Hello world (3)
が表示された直後であれば、次のようにして状態が 4 になっていることが確認できます。
iex(foo@emattsan)1> Agent.get(MockServer.Counter, & &1)
4
もう一度リクエストを送ってみます。
$ curl http://localhost:4000
Hello world (4)
状態が更新されていることがわかります。
iex(foo@emattsans-MBP)2> Agent.get(DummyServer.Counter, & &1)
5
リモートシェルで状態を変更してみます。
iex(foo@emattsans-MBP)3> Agent.update(DummyServer.Counter, & &1 + 10)
:ok
iex(foo@emattsans-MBP)4> Agent.get(DummyServer.Counter, & &1)
15
リクエストを送ると値が変更されていることがわかります。
$ curl http://localhost:4000
Hello world (15)
いつか読むはずっと読まない:ひみつシリーズ「かはくのひみつ」