HTTP クライアントとして Req をよく利用するのですが。
こういった外部の環境と接続する操作はテストが面倒なもの。
その点において Req はテストのための仕組みをパッケージ自身が提供してくれています。 その仕組みの使い方と、ちょっとした工夫の覚え書きです。
新しいプロジェクトを作って順を追って説明します。
$ mix new my_app $ cd my_app
Req を使う
まず Req の使い方のおさらいから。
mix.exs
の依存パッケージに Req を追加し、パッケージを取得します。
# ... defp deps do [ {:req, "~> 0.5"} ] end # ...
$ mix deps.get
Req を使う関数を追加します。
lib/my_app.ex
defmodule MyApp do def get(url) do Req.request(url: url) end end
追加した関数のテストを書きます。
test/my_app_test.exs
defmodule MyAppTest do use ExUnit.Case doctest MyApp describe "get/1" do test "get example.com" do assert {:ok, %Req.Response{status: 200, body: body}} = MyApp.get("https://example.com") assert body =~ ~r/Hello/ end end end
これでもテストとして実行できますが、実行するたびに指定した URL へのアクセスが発生します。 まずこれをスタブにします。
スタブを使う
Req はテストのために Req.Test
というモジュールを用意しています。
まずこのモジュールの利用を指定する設定をします。
plug: {Req.Test, MyApp}
は、HTTP リクエストのアダプタとして Req.Test
を MyApp
という名前で指定することを表しています。
指定の詳細については Req.new/1
のオプションの説明を参照してください。
設定を config/config.exs
に直接書いてもよいのですが、Config.import_config/1
を使った環境ごとに分離する定石に従うことにします。
config/test.exs
import Config config :my_app, req_options: [ plug: {Req.Test, MyApp} ]
config/config.exs
import Config import_config "#{config_env()}.exs"
また dev
と prod
のために config/dev.exs
と config/prod.exs
も作成しておきます。
これらは空のファイルで大丈夫です。
次に MyApp.get/1
を編集して設定した内容を利用するように変更します。
lib/my_app.ex
defmodule MyApp do def get(url) do [url: url] |> Keyword.merge(Application.get_env(:my_app, :req_options, [])) |> Req.request() end end
テストもスタブを利用するように変更します。
スタブは Req.Test.stub/2
で設定します。
第 1 引数は設定で指定した名前です。
第 2 引数は、Phoenix でもおなじみの Plug.Conn
の構造体を受け取り、レスポンスを返す関数です。
ここでは Req.Test.text/2
を使ってプレーンテキストを返していますが、他にも html/2
や json/2
といった関数が用意されています。
また Plug.Conn.put_status/2
などの Plug の関数を利用することも可能です。
test/my_app_test.exs
defmodule MyAppTest do use ExUnit.Case doctest MyApp describe "get/1" do test "get example.com" do Req.Test.stub(MyApp, fn conn -> Req.Test.text(conn, "Hello Req stub!") end) assert {:ok, %Req.Response{status: 200, body: body}} = MyApp.get("https://example.com") assert body =~ ~r/Hello/ end end end
最後に、依存パッケージに Plug を追加します。
今回はテストでだけ利用するので [only: :test]
オプションを指定しています。
mix.exs
# ... defp deps do [ {:req, "~> 0.5"}, {:plug, "~> 1.16", only: :test} ] end # ...
パッケージを取得してテストを実行します。
Req.Test.text/2
で指定したテキストが返されることが確認できると思います。
$ mix deps.get
$ mix test
setup を使う
スタブを設定するコードを ExUnit.Callbacks.setup/2
に移動して、繰り返し利用できるようにします。
加えてレスポンスのステータスとテキストもテストごとに設定できるように、 @tag
で指定できるようにしています。
test/my_app_test.exs
defmodule MyAppTest do use ExUnit.Case doctest MyApp setup context do body = Map.get(context, :body, "") status = Map.get(context, :status, 200) Req.Test.stub(MyApp, fn conn -> conn |> Plug.Conn.put_status(status) |> Req.Test.text(body) end) end describe "get/1" do @tag body: "Hello Req stub!" test "get example.com" do assert {:ok, %Req.Response{status: 200, body: body}} = MyApp.get("https://example.com") assert body =~ ~r/Hello/ end end end
Stub module を使う
setup も繰り返し利用できるように、モジュールに分離してみます。
まず、モジュールを追加して setup に書いた内容を移動します。
test/support/my_app_stub.ex
defmodule MyApp.Stub do defmacro __using__(_) do quote do setup context do body = Map.get(context, :body, "") status = Map.get(context, :status, 200) Req.Test.stub(MyApp, fn conn -> conn |> Plug.Conn.put_status(status) |> Req.Test.text(body) end) end end end end
今回はモジュールを use することで利用できるように __using__/1
マクロを利用しましたが、他にもっとよい方法があるかもしれません。
テストでは setup を削除して、追加したモジュールを use します。
test/my_app_test.exs
defmodule MyAppTest do use ExUnit.Case use MyApp.Stub doctest MyApp describe "get/1" do @tag body: "Hello Req stub!" test "get example.com" do assert {:ok, %Req.Response{status: 200, body: body}} = MyApp.get("https://example.com") assert body =~ ~r/Hello/ end end end
最後に、追加したモジュールがテストのときにだけコンパイルされるようにする設定を追加します。
mix.exs
# ... def project do [ # ... elixirc_paths: elixirc_paths(Mix.env()), # ... ] end # ... defp elixirc_paths(:test), do: ["lib", "test/support"] defp elixirc_paths(_), do: ["lib"] # ...
これでスタブを定義したモジュールを use するだけで繰り返しスタブが利用できるようになりました。