ちゃんゆーのブログ

思ったことや、技術系のメモなどを書き残していきます。

Symmetric API Testing を Python で試す

Python Advent Calendar 2021 の16日目の記事です。

以前、外部 API のテスト手法 Symmetric API Testing に関しての記事を書きました。

chanyou.hatenablog.jp

この記事では、それを Python で実践する方法についてまとめてみます。

Symmetric API Testing とは?

改めて Symmetric API Testing とは

Symmetric API Testing は実際のリクエストとレスポンスを自動で保存しておいて、テスト実行時に保存したデータを参照する手法

Symmetric API Testing という、手間なく堅牢に外部 API Client をテストする手法 - ちゃんゆーのブログ

のことです。

完全なモックを実装すると、モックのメンテナンスコストがかかってきます。 かといってテスト実行時に都度 API を呼び出すと、テストの実行に時間がかかるだけでなく、レート制限や一時的なエラーでテストが失敗することがあります。もちろん API 側に不必要な負荷をかけてしまうので、できれば避けたいところです。

Symmetric API Testing の手法は、上記の中間を取るような方法になってきます。詳細については上記の記事をご参照ください。

Python で Symmetric API Testing をやるには?

リクエストとレスポンスを対応するように保存しておく仕組みを用意できれば、どういった言語、アーキテクチャでも Symmetric API Tesing を実践することは可能なはずです。

そういったことをやってくれるライブラリは各言語で公開されており、Python の場合は VCR.py というライブラリを使うのが便利です。

github.com

VCR.py の簡単な使い方

詳細の使い方は VCR.py 📼 — vcrpy 4.0.2 documentation をご覧いただくとして、本当に簡単な使い方だけご紹介します。

インストール

$ pip install vcrpy

with 句で使う

import vcr
import requests


with vcr.use_cassette("fixtures/sample.yaml"):
    response = requests.get("https://jsonplaceholder.typicode.com/todos/1")
    todo = response.json()
    assert todo["id"] == 1
$ python main.py

with 句で囲むだけで、あとは通常通りリクエストを投げれば OK。

実行後、use_cassette で指定したパスに yaml ファイルが保存されています。

interactions:
- request:
    body: null
    headers:
      <省略>
    method: GET
    uri: https://jsonplaceholder.typicode.com/todos/1
  response:
    body:
      string: !!binary |
        H4sIAAAAAAAAA6vmUlBQKi1OLfJMUbJSMNQBcTMRzJLMkpxUJSsFpZTUnNTkktJihcTSEhBOzVUC
        K0jOzy3ISS1JBWlJS8wpTuWqBQA33AG3UwAAAA==
    headers:
      <省略>
    status:
      code: 200
      message: OK
version: 1

これがレスポンスの実体になります。 VCR.py ではカセットファイルと呼ばれています。

2回目以降は自動的にカセットファイルを読み込んで実行してくれます。インターネットから切断しても、問題なく実行できます。

デコレータで使う

import vcr
import requests


@vcr.use_cassette("fixtures/test_todo.yaml")
def test_todo():
    response = requests.get("https://jsonplaceholder.typicode.com/todos/1")
    todo = response.json()
    assert todo["id"] == 1


if __name__ == "__main__":
    test_todo()

実際には unittest や pytest に統合して使うことになりますが、関数やメソッドにデコレータを加えるだけで、 Symmetric API Testing が実現できます。

カセットファイルのパスをベタ書きで指定していますが、 VCR の設定で関数名から自動で命名して指定したパスに保存することもできるので、最初の設定さえ済めばストレスなく利用することができます。

使用感

  • 設定項目がそれほど多くなく、手軽に導入できる
  • pytest や unittest の統合が手軽にできる
  • リクエストヘッダやボディをマスクすることができて便利

pytest や unittest により手軽に導入するための vcrpy-unittestpytest-vcr というパッケージが公開されていて、必要に応じて利用すると便利です。

そして特に便利なのが フィルタ機能 で、センシティブなデータを除いてカセットファイルに保存することができます。

with my_vcr.use_cassette('test.yml', filter_headers=['authorization']):
    # sensitive HTTP request goes here

https://vcrpy.readthedocs.io/en/latest/advanced.html#filter-information-from-http-headers より引用

この機能を使うと Bearer Token を XXXXXX に置換してカセットファイルに保存するといったことが簡単に実現できます。便利です。

これによってカセットファイルをそのまま git リポジトリ管理下におくことができるので、よりテストが実行環境に依存しにくくなります。

どう運用していくか?

導入は簡単にできそうですが、どう運用していくのが良いのでしょうか。 カセットファイルは一度作成したらそのままで良いのでしょうか。

正直なところ自分も模索中ではあるのですが、何を重点的にテストしたいのかに依りそうです。

  • アプリケーションの変更の影響をテストしたい
  • 外部 API の変更をなるべく早く検知したい

前者は通常テストが担うべき領域だと思います。

一方で後者は、例えば外部 API の変更、特にドキュメンテーションされていないようなサイレントな変更がアプリケーションに影響を及ぼしていないか把握したいシチュエーションです。例えば TwitterBot など、外部 API に強く依存するアプリケーションの場合には、外部 API の変更による動作確認は行いたいはずです。

VCR.py でテストを行う場合、カセットファイルが古いままだと、テストは通るが本番で動作しない可能性があります。

カセットファイルを定期的に再生成する運用

外部 API の変更をなるべく早く検知したいのであれば、カセットファイルを定期的に再生成する運用が考えられます。

色々やり方はあるかと思いますが、 要件に応じて日次、週次、月次などのタイミングで CI/CD 起点に以下のフローを実行するのが良さそうです。

  1. カセットファイルを削除する
  2. テストを実行してカセットファイルを再生成する
  3. (テスト落ちたら)チケット切ったり、カセットファイルの差分を含めて PR 作成したりする

カセットファイルを作りっぱなしにする運用

逆に、依存している外部 API が安定していて、影響も限定的である場合は、コストかけずにカセットファイルを作りっぱなしにする運用がリーズナブルです。

まとめ

Symmetric API Testing を Python で実践するために、 VCR.py の導入と運用についてまとめました。

導入はともかく、運用についてまだまだ工夫できそうなので、うまいやり方あったら教えてほしいです。

おわり。