(この記事は、「fukuoka.ex Elixir/Phoenix Advent Calendar Advent Calendar 2018」の12日目です)
昨日は @twinbee さんのgrpc-elixirでGoと通信してみる #1でした。
今日は私からElixirでのStub実装のお話です。
Stub実装が欲しくなるケース
業務システムの開発をしていると、よそ様の開発したシステムに連結しなければ処理が実行できない要件などがしばしば登場します。
そうした場合、テスト実行のたびに相手のシステムへアクセスしたり、更新をかけたりすることが望ましくない場合も多く、自動テストの妨げになる事もあります。
そういったケースにおいてAdapterとStubを実装しておく事でテスト時の好ましくない依存関係を解消することができます。
以下で実装の流れを解説します。
プロジェクトを作成する
まずはサンプル実装のためのプロジェクトを実装します。
Elixirのバージョン確認します。
> elixir --version
Erlang/OTP 20 [erts-9.2.1] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
Elixir 1.6.1 (compiled with OTP 20)
Phoenixプロジェクトを作成します。
> mix phx.new stub_sample --no-ecto --no-brunch
> cd stub_sample
mix定義に必要なライブラリを追加してdeps.getします。
def application do
[
mod: {StubSample.Application, []},
extra_applications: [:logger, :runtime_tools, :httpoison] # <- :httpoison追加
]
end
defp deps do
[
{:phoenix, "~> 1.3.2"},
{:phoenix_pubsub, "~> 1.0"},
{:phoenix_html, "~> 2.10"},
{:phoenix_live_reload, "~> 1.0", only: :dev},
{:gettext, "~> 0.11"},
{:cowboy, "~> 1.0"},
{:poison, "~> 3.0"}, # <- 追加
{:httpoison, "~> 1.4"}, # <- 追加
]
end
> mix deps.get
まずは直接実装してみる
httpoisonを使ってQiitaのタイトル一覧を取得する簡単な処理を実装してみます。
defmodule StubSample.ApiAdapter.Qiita do
def list_items(url) do
resp = HTTPoison.get! url
{:ok, contents} = Poison.decode(resp.body)
Enum.map(contents, fn(content) -> content["title"] end)
end
end
実行するとタイトル一覧が取得できます。
iex(1)> StubSample.ApiAdapter.Qiita.list_items("https://qiita.com/api/v2/items?page=1&per_page=10&query=Fukuoka.ex")
["【超残念】fukuoka.ex Elixir/Phoenix Advent Calendar 2018参加を断念",
"[Elixir]Calendarモジュールを使って、DataFormatを作る方法を色々と試す。",
"grpc-elixirでGoと通信してみる #1",
"Elixirでニューラルネットワークを実装しようとした話",
"Elixir(エリクサー)で数値計算すると幸せになれる",
"プログラマでも「Figma」ならレイアウトやロゴが簡単にデザインできる",
"\"show weather\" コマンド作ってみた",
"データサイエンスプラットフォームEsunaの前処理UI/Elixirコードは、入力データ解析の後、データ内容から自動生成されている",
"RustElixirで線形回帰を高速化した話",
"【doctestつき】AtCoder に登録したら解くべき精選過去問 10 問を\"Elixir\"で解いてみた"]
AdapterとStubを実装する
先ほどの実装では、Qiitaにアクセスできない状態ではテストができません。
独立したテストができるようにAdapterとStubモジュールを実装していきます。
callbackとしてlist_items()/1を定義したモジュールを実装します。
defmodule StubSample.ApiAdapter do
@callback list_items(url :: String) :: List
end
Qiitaアダプターにbehaviour定義を追加します。
defmodule StubSample.ApiAdapter.Qiita do
@behaviour StubSample.ApiAdapter # <- 追記
def list_items(url) do
resp = HTTPoison.get! url
{:ok, contents} = Poison.decode(resp.body)
Enum.map(contents, fn(content) -> content["title"] end)
end
end
同じくStubSample.ApiAdapterを定義してlist_items()/1を実装したStubモジュールを実装します。
defmodule StubSample.ApiAdapter.Stub do
@behaviour StubSample.ApiAdapter
def list_items(url) do
["#{__MODULE__} stub was worked! #{url}"]
end
end
上記の実装を呼ぶビジネスロジックを想定したモジュールを実装します。
list_itemsを実行するモジュールは直接aliasで定義せず、
configから取得するようにしています。
defmodule StubSample.ApiCallSample do
@adapter Application.get_env(:stub_sample, StubSample.ApiAdapter)[:adapter_module]
def api_call(url) do
@adapter.list_items(url)
end
end
Configを定義する
ApiAdapterの実装モジュールとして
devではStubSample.ApiAdapter.Qiitaを、
testではStubSample.ApiAdapter.Stub
を定義します。
config :stub_sample, StubSample.ApiAdapter,
adapter_module: StubSample.ApiAdapter.Qiita
config :stub_sample, StubSample.ApiAdapter,
adapter_module: StubSample.ApiAdapter.Stub
実行してみる
まずはMIX_ENVを指定せずに(MIX_ENV=dev)で実行してみるとStubSample.ApiAdapter.Qiitaの処理が実行されます。
> iex -S mix
iex(1)> StubSample.ApiCallSample.api_call("https://qiita.com/api/v2/items?page=1&per_page=10&query=Fukuoka.ex")
["【超残念】fukuoka.ex Elixir/Phoenix Advent Calendar 2018参加を断念",
"[Elixir]Calendarモジュールを使って、DataFormatを作る方法を色々と試す。",
"grpc-elixirでGoと通信してみる #1",
"Elixirでニューラルネットワークを実装しようとした話",
"Elixir(エリクサー)で数値計算すると幸せになれる",
"プログラマでも「Figma」ならレイアウトやロゴが簡単にデザインできる",
"\"show weather\" コマンド作ってみた",
"データサイエンスプラットフォームEsunaの前処理UI/Elixirコードは、入力データ解析の後、データ内容から自動生成されている",
"RustElixirで線形回帰を高速化した話",
"【doctestつき】AtCoder に登録したら解くべき精選過去問 10 問を\"Elixir\"で解いてみた"]
次に、MIX_ENV=test で実行すると、StubSample.ApiAdapter.Stubの処理が実装されます。
> MIX_ENV=test iex -S mix
iex(1)> StubSample.ApiCallSample.api_call("https://qiita.com/api/v2/items?page=1&per_page=10&query=Fukuoka.ex")
["Elixir.StubSample.ApiAdapter.Stub stub was worked! https://qiita.com/api/v2/items?page=1&per_page=10&query=Fukuoka.ex"]
(2018/12/13 実行イメージがStubを直接実行しているケースを記載していたので修正しました。)
まとめ
- @call_back と @behaviour で振る舞いを定義
- cofigとApplication.get_env()を使えば環境毎にモジュールを切り替えることができる。
将来的に複数のサービスを環境毎に切り替えるようなケースでもこのパターンは使えるかと思います。
明日のfukuoka.ex Elixir/Phoenix Advent Calendar 2018 14日目の記事は, @kikuyuta さんの階段の上でも下でも電灯を点けたり消したりするです。こちらもお楽しみに!