目次
天気予報API
以下をdepsに追加
{:mox, "~> 1.0", only: :test},
{:req, "~> 0.1"},
libとtestディレクトリはこのような構成にする予定です。
lib
├ weather.ex
└ weather
├ behaviour.ex
└ impl.ex
test
├ test_helper.ex
└ weather_test.ex
Mock化とは
外部APIにアクセスする関数のテストを記述する際、通常の実行と同じく外部APIにアクセスする必要があります。
そうなるとテストの実行時間が伸びてしまい、全体のテストの終了時間も遅くなってしまう問題が発生します。
そういった関数はMock化することによって外部APIを利用せずに済み、テスト実行時間を省略することが可能になります。
Mock化とは関数の処理を模倣することで、Behaviour(振る舞い、出力がどのようなものであるか)を定義し、expect(期待通りの振る舞い)を設定することです。
ElixirでMockを定義するライブラリがMoxである。
使用するメリット
まず外部APIには呼び出す状況(日付など)で出力結果が変わるものがあります。
例として天気予報や株価のデータを取得するAPIなどが挙げられます。
Mockを使用すると安定した出力結果が得られ、出力結果を使用した画面描画などの再現が可能になります。
また外部APIがエラーを出力したときも再現できるので、耐障害性を上げることができます。
基本的な使い方
次の3ステップで関数のMock化を行う
- Behaviourを定義する
- Behaviourを実装した関数を作成する。
- 実装をカプセル化する
- MockをBehaviourに基づいて設定する。
- テストを作成し、Mockのexpectを設定する
Mock化を行うにあたって次のファイルを作成します。
ファイル | 説明 |
---|---|
/lib/weather/behaviour.ex | Behaviorを作成 |
/lib/weather/impl.ex | Behaviorを実装 |
/lib/weather.ex | 実装をカプセル化、ここから呼び出す。 |
/test/weather.ex | Mockのexpectを設定してテストを行う。 |
Behaviour(動作)を定義する
Mockを作成するには、関数とMockが同じBehaviourを実装している必要があります。
では天気予報APIを使用して、明日の天気を取得する関数であるget_weather/1を作りましょう。
get_weather/1は都市IDを受け取り、{:ok, 明日の天気}
か{:error, エラー文}
を出力するものとします。
この要件に沿ってBehaviourを設定すると次のようになります。
defmodule Weather.Behaviour do
@callback get_weather(integer()) :: {:ok, String.t()} | {:error, String.t()}
end
Behaviourを実装した関数を作成する。
(1)で定義したBehaviourを実装した関数を作成します。
用語
- @behaviour どのBehaviourを実装するかを指定
- @impl どのBehaviourの関数を実装するかを指定
defmodule Weather.Impl do
alias Weather.Behaviour
@behaviour Behaviour
@impl Behaviour
def get_weather(city_id) do
case Req.get!("https://weather.tsukumijima.net/api/forecast?city=#{city_id}").body do
%{"error" => msg} ->
{:error, msg}
%{"forecasts" => forecasts} ->
{:ok, Enum.at(forecasts, 1)["detail"]["weather"]}
end
end
end
実装をカプセル化する
Mock化するためには、Mock化する関数の処理を結果のみが帰ってくるようにする必要があります。
つまり次のようなものとする必要があります。
def Mock関数(引数) do
# 値のみを返す
end
Mockのexpectをするためには、関数の処理を単純化する必要があります。
そのために実装のカプセル化を行い、外部APIの呼び出しを隠蔽します。
カプセル化を行うと以下のようになります。
defmodule Weather do
def get_weather(city_id) do
weather_impl().get_weather(city_id)
end
defp weather_impl() do
Application.get_env(:weather, :call)
end
end
ここでは外部依存性を減らすために、config.exから実装を呼び出しています。
ここでconfig.exファイルには以下を追記します。
import Config
# 追加
config :weather, :call, Weather.Impl
MockをBehaviourに基づいて設定する。
Mockを定義するには/test/test_helper.exs
に以下を追記します。
Mox.defmock(WeatherBehaviourMock, for: Weather.Behaviour)
Application.put_env(:weather, :call, WeatherBehaviourMock)
Mox.defmockでBehaviourを元に、Mockを定義しています。
またApplication ~
の行で、test内ではWeatherはWeatherBehaviourMockを呼び出します。
テストを作成し、Mockのexpectを設定する
では実際にテストを作成します。
テストを作成する際に次の3つを記述します。
- use ExUnit.Case
- import Mox
- setup :verify_on_exit!
詳しくはこちら
https://hexdocs.pm/mox/Mox.html#verify_on_exit!/1
Moxを使用したテストの記述例が次のようになります。
defmodule WeatherTest do
use ExUnit.Case
import Mox
setup :verify_on_exit!
test "get_weather/1" do
expect(WeatherBehaviourMock, :get_weather, fn city_id ->
assert city_id == 400040
{:ok, "はれ ときどき ぶた 所により ビーム"}
end)
assert {:ok, _} = Weather.get_weather(400040)
end
end
通常Weather.get_weather(400040)
は、Weather.Impl.get_weather
で実行しているため実行した日などによって出力結果が異なります。
しかしここではWeatherBehaviourMock.get_weather
を呼び出しており、戻り値はexpectで設定した値が出力されています
したがって、特定の出力結果の場合のテストが可能になるということです。
結論
外部APIを使用する処理はMoxでテストを行う。
なぜなら、テストをする際に、特定の値が帰ってきたときのテストが記述できるから。
以上