🧪

外部APIのテストを楽に行うpytestのプラグイン pytest-recordingの紹介

2024/12/05に公開

こんにちは!Septeni Japan 株式会社のエンジニアの大志万といいます。

開発中に、外部の WEB API を使ってサービスを構築する機会は多いと思いますが、
API を使った場合、下記のような理由でテストが難しいことがあります。

  • レートリミットがあり、テストのたびにリクエストを送りたくない
  • テスト環境が分離されておらずテストデータが日常的に変化してしまうため、テストの再現性が確保しにくい

こういった課題を解決するためには、Mockや開発環境を構築することが一般的ですが、
数が増えてくるとメンテナンスが大変になってきます。

そこで、今回は pytest-recording というライブラリを使って、APIのレスポンスを記録し、Mockとして再利用する方法を紹介します。
APIのレスポンスが複雑でMock化するのが難しい場合でも、このライブラリを使ったら簡単にMockとして再利用することが可能です。

pytest-recording とは

pytest-recordingはVCR.pyをpytest用にラップしたライブラリです。
そのため、レスポンスの記録や再利用と言ったコア機能はVCR.pyに依存しています。

Rubyにはvcrというライブラリがあり、VCR.py はそのPython版のようです。

ちなみにVCRは何の略かというと、Video Cassette Recorder の略です。
そのため、VCR.py のアイコンはビデオカセットを表したものになっています。

https://github.com/kiwicom/pytest-recording

https://github.com/kevin1024/vcrpy

使用方法

前提

  • pytest を使用していること

インストール

pip install pytest-recording

テストの実行

対象のテストに pytest.mark.vcr デコレータを保存したいテストに付与するだけでOKです。
あとは通信している内容をpytest-recordingが自動でキャプチャしてくれます。

tests/test_sample.py
@pytest.mark.vcr
def test_sample():
    res = requests.get("https://jsonplaceholder.typicode.com/todos/1").json()
    assert res["userId"] == 1

初回実行時には--record-mode=once をつけて実行します。(レコードモードについては後述)

pytest --record-mode=once tests/

実行すると cassettes ディレクトリとその中にファイルが作成され、テストのレスポンスが保存されます。

tests/cassettes/test_sample/test_sample.yaml
interactions:
- request:
    body: null
    headers:
      Accept:
      - '*/*'
      Accept-Encoding:
      - gzip, deflate
      Connection:
      - keep-alive
      User-Agent:
      - python-requests/2.32.3
    method: GET
    uri: https://jsonplaceholder.typicode.com/todos/1
  response:
    body:
      string: "{\n  \"userId\": 1,\n  \"id\": 1,\n  \"title\": \"delectus aut autem\",\n
        \ \"completed\": false\n}"
    headers:
      ...
    status:
      code: 200
      message: OK
version: 1

以降は、pytest を実行するだけで、保存されたレスポンスを再利用してテストを実行してくれます。

レコードモード

pytest-recording には、5 つのレコードモードがあります。
レコードモードを指定することで、pytest-recording の挙動を変更することが可能です。

once

  • カセットファイルがあり、通信が記録されている場合、リクエストを送る代わりに記録されているレコードを使用する。
  • カセットファイルがあるが、対象の通信が記録されていない場合、エラーになる。
  • カセットファイルがない場合、行われた通信を新しくカセットファイルに記録する

例えば、test_sampleに新しく、リクエストを追加して、record-mode-once で再実行するとCannotOverwriteExistingCassetteExceptionが発生します。

tests/test_sample.py
@pytest.mark.vcr
def test_sample():
    res = requests.get("https://jsonplaceholder.typicode.com/todos/1").json()
+   res2 = requests.get("https://jsonplaceholder.typicode.com/todos/2").json()
    assert res["userId"] == 1
+   assert res2["userId"] == 2

cannnot_overwrite_existing

基本的には once を使うのが、ファイルの上書きも防げるので良い気がしています。

new_episodes

  • カセットファイルがあり、通信が記録されている場合、リクエストを送る代わりに記録されているレコードを使用する。
  • カセットファイルがあるが、対象の通信が記録されていない場合、新しく通信を記録するようにする。
  • カセットファイルがない場合、行われた通信を新しくカセットファイルに記録する

once と似ていますが、once が通信が記録されていない場合、エラーになるのに対して new_episode は新しく通信を記録するようになります。

テスト内で、頻繁にリクエスト先が変わるケースでは new_episodes を使うと便利そうです。

all

  • カセットファイルの有無にかかわらず、すべての通信を行い、新しく記録する

古いレコードを一気に新しくするためには all を使うのが便利そうです。

none

  • カセットファイルに通信が記録されている場合、リクエストを送る代わりに記録されているレコードを使用する。
  • 記録されていない場合はエラーを発生させる。

none は、新たなHTTPリクエストが行われないことを保証するモードです。
そのため、レートリミットが厳しいAPIを使う場合など、なるべく通信させたくない場合はデフォルトを nones にしておくのが良さそうです。

ちなみに、none はpytest-recordingのデフォルトのモードです。

rewrite

  • カセットファイルの有無にかかわらず、すべての通信を行う。
  • カセットファイルに通信が記録されている場合、レコードの内容を上書きする。新しいレコードの追加は行わない。

rewrite は、カセットファイルの内容を更新したい場合に使うと便利です。all とは違い、既にあるレコードを上書きするだけなので、新しいレコードの追加は行われません。

ちなみに rewrite 以外は VCR.py にもあるレコードモードですが、
rewrite は pytest-recording 独自の機能になります。


上記で紹介したレコードモードは、--record-modeオプションで指定する以外にも、後述する vcr_config Fixture で定義することが可能です。

vcr_config Fixture

vcr_config Fixture を定義することでより細かな制御ができるようになります。

@pytest.fixture
def vcr_config():
    return {
        "record_mode": "once",
        "filter_headers": ["authorization"],
        "ignore_hosts":["jsonplaceholder.typicode.com"]
    }

dict でVCR.pyの設定を行うことができます。

ここではいくつかの設定を紹介します。

filter_headers

github にレコードを push するときに、アクセストークンなどセンシティブなデータは保存したくないというケースがあると思います。
filter_headers を使うことで、指定したヘッダーを保存しないようにすることができます。

ignore_hosts

特定のホストに対する通信について、レコードで保存しないようにすることができます。
例えば AWS SecretManager などからトークンを取得し、APIにアクセスするテストをしたいとなった場合、ignore_hostsにSecret Managerのホストを指定すると、その通信はレコードされなくなります。

before_record_response

もし、何かレスポンスのデータに変更を加えたい場合は、before_record_responseキーに関数を設定することで編集が可能です。
レスポンスではなく、関数の実行の結果を保存するようになります。
(引数にはレスポンスの結果を受け取ります。)

例えば、headersを削除したレスポンスを保存したい場合は下記のように設定すると、
headersが消えたデータをcassetteとして記録できます。

def before_record_response(response):
    response['headers'] = {}
    return response

@pytest.fixture
def vcr_config():
    return {
        "before_record_response": before_record_response
    }

まとめ

今回は、pytest-recording を使って API テストを自動化する方法について紹介しました。pytest-recording を使うことで、外部 API へのリクエストをキャプチャし、再利用することができるため、テストの再現性を高めることができます。また、レコードモードを活用することで、テストの挙動を柔軟に制御することができ、レートリミットやレスポンスの変動に悩まされることなく、効率的にテストを行うことができます。

しかし、キャプチャしたレスポンスが古くなった場合、実際の API の挙動と乖離する可能性があります。そのため、バージョンアップや仕様変更のタイミングで定期的にレコードを更新してください。(更新にはrewriteコマンドが有用です)また、センシティブな情報を含むレスポンスを保存し、Githubなどに push する際には、ignore_hostsやfilter_headersを活用して、セキュリティを確保が必要なことに注意してください。

最後までお読みいただき、ありがとうございました。質問やフィードバックがありましたら、ぜひコメント欄でお知らせください。

Discussion