Elixirで簡単HTTPモック: ExVCRの基本と使い方

## はじめに

本記事では、Elixir 開発者の「HTTP クライアントのテストをもっと簡単にしたい」という悩みに応える [ExVCR] を紹介します。

ExVCR は、テスト時に実際の HTTP リクエストを録画・再生し、ネットワークアクセスなしで再現性のあるテストを可能にする強力なツールです。
以下のような場面で役立ちます。

- 外部 API の応答を再現したい
- ネットワーク障害時にもテストを実行したい
- 予期せぬ API の変更を検出したい

## ExVCR の基本

[ExVCR]は、Ruby の VCR にインスパイアされた Elixir 用 HTTP モックライブラリです。
**HTTP リクエストとそのレスポンスを録画 (record)・再生 (replay)** することで、再現性のあるテストを簡単に実現します。

https://qiita.com/mnishiguchi/items/244a10f44e61ec9427d9

主な特徴は以下のとおりです。

- **カセットベースのアプローチ**: リクエストとレスポンスを「カセット」として保存し、必要に応じて再生
- **多様な HTTP ライブラリをサポート**: [Finch](https://hexdocs.pm/finch/readme.html)、[HTTPoison](https://hexdocs.pm/httpoison/HTTPoison.html)、[:httpc](https://www.erlang.org/doc/man/httpc.html) など複数のライブラリに対応
- **設定可能なフィルタリング**: 秘密情報（例: API キー）をマスキング可能
- **簡単なセットアップとカスタマイズ**: デフォルトの設定でも即使用可能

### 同様の状況での代替アプローチ

- **手動でモック**: 手動でモックを作成して HTTP クライアントの挙動を模倣する方法です。モックを使用すると、外部 API への依存を取り除くことができますが、リクエストやレスポンスの詳細を正確に再現するのが難しい場合があります。
- **Bypass を利用**: [Bypass] を使うと、テスト用にローカルサーバーを立ち上げ、リクエストをキャプチャして処理を模倣できます。これにより、テストをよりリアルに近づけることができますが、セットアップが煩雑になりがちです。
- **リアル API との統合テスト**: 実際のエンドポイントを利用してテストすることで、外部 API の挙動をそのまま確認できます。ただし、この方法はネットワーク状態に依存し、API が不安定な場合や変更があった場合にテストが失敗するリスクがあります。

これらの方法はそれぞれ一長一短がありますが、管理や保守の手間を考慮すると、[ExVCR]が非常に便利です。
[ExVCR]を利用すれば、リクエストとレスポンスを録画して保存しておくだけで、簡単にテストを再現できます。

HTTP リクエストとレスポンスは JSON 形式で保存されます。デフォルトでは`vcr_cassettes`ディレクトリに自動保存されます。一方、手動で管理したい場合は`custom_cassettes`ディレクトリを利用することができます。`vcr_cassette_library_dir`および`custom_cassette_library_dir`の設定項目を使って保存先を変更することも可能です。

> 踏み出せば、その一足が道となる。

まずは[ExVCR]のセットアップに挑戦してみましょう！

## インストールとセットアップ

### インストール

以下を `mix.exs` の `deps` に追加します。

```elixir
defp deps do
  [
    {:exvcr, "~> 0.15.2", only: :test},
    {:req, "~> 0.5.6"} # HTTPクライアント用
  ]
end
```

次に、依存関係を取得します。

```bash
mix deps.get
```

### 初期設定

[ExVCR]はデフォルト設定のままで十分に動作します。初めて使用する場合は追加設定を行わず、そのまま動作確認を進めて問題ありません。

特定の要件でデフォルトの動作を上書きしたい場合のみ、`config/config.exs` に手動設定が必要です。

例えば、デフォルトでは、カセットはプロジェクトのルートディレクトリ直下に保存されますが、保存先を明示的に`test/fixture/vcr_cassettes`に変更する場合には以下のように設定します。

```elixir
import Config

config :exvcr,
  vcr_cassette_library_dir: "test/fixture/vcr_cassettes"
```

詳細は、[公式ドキュメント](https://hexdocs.pm/exvcr)をご参照ください。

## シンプルな例

例として、https://jsonplaceholder.typicode.com のダミー API からデータを取得するテストを作成します。

### API クライアントモジュール

```elixir
defmodule HelloExvcr.ApiClient do
  @api_base_url "https://jsonplaceholder.typicode.com"

  def get_todo(id) do
    Req.get!("#{@api_base_url}/todos/#{id}")
  end
end
```

### テストモジュール

```elixir
defmodule HelloExvcr.ApiClientTest do
  use ExUnit.Case
  use ExVCR.Mock, adapter: ExVCR.Adapter.Finch

  setup do
    ExVCR.Config.cassette_library_dir("test/fixture/vcr_cassettes")
    :ok
  end

  test "get_todo" do
    use_cassette "get_todo_example" do
      {:ok, response} = HelloExvcr.ApiClient.get_todo(1)
      assert response.body["title"] == "delectus aut autem"
    end
  end
end
```

最初のテスト実行でカセットが作成され、その後はカセットを使ったテストが実行されます。

## 応用例

### カスタムマッチャー

[ExVCR]では、デフォルトのリクエスト比較条件に加えて、カスタムマッチャーを使用して特定のニーズに応じた比較ロジックを設定できます。たとえば、クエリパラメータや特定のヘッダーに基づいてリクエストを区別する場合に便利です。これにより、より柔軟なテストケースの記述が可能になります。

```elixir
defmodule HelloExvcr.ApiClient do
  @api_base_url "https://jsonplaceholder.typicode.com"

  def get_todo_with_params(id, params) do
    url = "#{@api_base_url}/todos/#{id}"
    Req.get!(url, params: params)
  end
end

defmodule HelloExvcr.ApiClientTest do
  use ExUnit.Case
  use ExVCR.Mock, adapter: ExVCR.Adapter.Finch

  test "get_todo_with_custom_matcher" do
    use_cassette "custom_matcher_example", match_requests_on: [:query, :headers] do
      response =
        HelloExvcr.ApiClient.get_todo_with_params(1, %{
          "filter" => "completed",
          "userId" => 123
        })

      assert response.status == 200
      assert response.body["title"] == "delectus aut autem"
    end
  end
end
```

### 秘密情報の除外

API キーやアクセストークンなどの秘密情報をテストカセットに記録しないようにするために、フィルタリング機能を利用できます。この機能では、指定したパターンをプレースホルダーに置き換えることで、セキュリティを確保しながらカセットを共有できます。

以下の設定例では、`API_TOKEN`という環境変数の値を`<API_TOKEN>`に置き換えます。

```elixir
config :exvcr,
  filter_sensitive_data: [
    [pattern: System.fetch_env!("API_TOKEN"), placeholder: "<API_TOKEN>"]
  ]
```

### リクエストボディやヘッダーの比較

[ExVCR]では、リクエストボディやヘッダーの内容を細かく比較する設定が可能です。これにより、同じエンドポイントに対して異なるパラメータやヘッダーで行われたリクエストを区別できます。この機能は、複雑な API のテストで非常に役立ちます。

具体例として、以下のようにカスタムロジックを組み込むことで、比較条件を詳細に定義できます。

```elixir
use_cassette "example", match_requests_on: [:query, :headers] do
  response = HelloExvcr.ApiClient.get_todo_with_params(1, %{filter: "completed"})
  assert response.status == 200
end
```

https://hexdocs.pm/exvcr/ExVCR.Mock.html#use_cassette/3

### カスタムカセット

ExVCR では、用途に応じて複数のカセット保存先を設定し、個別に管理することができます。
例えば、テストケースごとに異なるディレクトリにカセットを保存する場合、以下のようにカスタムカセットディレクトリを指定できます。

```elixir
config :exvcr,
  custom_cassette_library_dir: "test/fixture/custom_cassettes"
```

テストモジュール内では以下のように使用します。

```elixir
use_cassette "custom_cassettes/api_client/test_specific_case" do
  response = HelloExvcr.ApiClient.get_todo(123)
  assert response.status == 200
end
```

この設定では、`test/fixture/custom_cassettes/api_client/test_specific_case.json`にカセットが保存されます。

## おわりに

[ExVCR]を使うことで、HTTP テストが驚くほど簡単になります。本記事を参考に、テストの効率化に挑戦してみてください。

この記事のコードは GitHub リポジトリで公開しています。以下をご覧ください:

https://github.com/mnishiguchi/hello_exvcr

何か氣づいた点があれば、コメントで共有していただけると嬉しいです。

![toukon-qiita-macbook_20230912_091808.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82804/fd5c55ec-4fe0-8af6-59bc-bab1ef3d182b.jpeg)

[ExVCR]: https://github.com/parroty/exvcr
[Bypass]: https://github.com/PSPDFKit-labs/bypass