エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

Reqのテストを書く覚書

HTTP クライアントとして Req をよく利用するのですが。

hex.pm

こういった外部の環境と接続する操作はテストが面倒なもの。

その点において 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 というモジュールを用意しています。

hexdocs.pm

まずこのモジュールの利用を指定する設定をします。

plug: {Req.Test, MyApp} は、HTTP リクエストのアダプタとして Req.TestMyApp という名前で指定することを表しています。 指定の詳細については 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"

また devprod のために config/dev.exsconfig/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/2json/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 を追加します。

hex.pm

今回はテストでだけ利用するので [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 するだけで繰り返しスタブが利用できるようになりました。