TT Lab
はじめる
学ぶ 学習パス コース

テストツール実戦

障害を注入してリソース漏れを見つける

TT Labで続きを見る

目標

正常・二重終了・失敗の経路と、アプリのlifespanの開始・後始末を観測します。

なぜ重要なのか

リクエストが成功したときは接続を閉じるコードがありましたが、例外が発生すると開いた接続が残りました。成功レスポンスだけを確認するテストでは、このリークは表に出ませんでした。ライフサイクルのテストは、戻り値よりも、リソースがいつ開き、いつ閉じたかに集中する必要があります。

ステップ

  1. /root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: new_resource()は{open:False, events:[]}の新しい辞書で、呼び出し同士でeventsを共有しません。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

最初に1回だけ準備してください。既存のファイルは上書きしません。

mkdir -p /root/work/test-lifespan-cleanup-lab
test -e /root/work/test-lifespan-cleanup-lab/service.py || cp /opt/fixtures/ten_labs/test-lifespan-cleanup-lab/service.py /root/work/test-lifespan-cleanup-lab/service.py
test -e /root/work/test-lifespan-cleanup-lab/test_service.py || cp /opt/fixtures/ten_labs/test-lifespan-cleanup-lab/test_service.py /root/work/test-lifespan-cleanup-lab/test_service.py
cd /root/work/test-lifespan-cleanup-lab
  1. /root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: start(resource)は、すでに開いていればValueError、そうでなければopen=Trueに変えてeventsに'open'を追加します。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

  2. /root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: stop(resource)は、開いているときだけopen=Falseに変え、'close'をeventsに追加します。すでに閉じていればそのままにします。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

  3. /root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: read(resource)は、閉じていればRuntimeError、開いていれば{ready:True}を返します。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

  4. /root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: scope(resource)はcontextmanagerです。入るときにstartし、ブロックの中にはresourceをyieldし、ブロックの成功・失敗のどちらでもstopで閉じます。ブロックの例外は伝播させます。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

  5. /root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: lifespan_for(resource)は、asynccontextmanagerの関数lifespan(app)を返します。scope(resource)の中でapp.state.resourceを設定してyieldします。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

  6. /root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: create_app(resource)は、lifespan_forを使います。GET /readyは、app.state.resourceをreadした結果を返します。contextの終了時にリソースを閉じる必要があります。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

  7. /root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: exercise(resource, fail=False)は、with TestClient(create_app(resource))の中でGET /readyを呼び出します。fail=Trueならその中でRuntimeErrorを送出し、そうでなければレスポンスのJSONを返します。どちらの場合もリソースが閉じている必要があります。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

参考

リソースの状態を独立させる(テスト)

/root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: new_resource()は{open:False, events:[]}の新しい辞書で、呼び出し同士でeventsを共有しません。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

最初に1回だけ準備してください。既存のファイルは上書きしません。

mkdir -p /root/work/test-lifespan-cleanup-lab
test -e /root/work/test-lifespan-cleanup-lab/service.py || cp /opt/fixtures/ten_labs/test-lifespan-cleanup-lab/service.py /root/work/test-lifespan-cleanup-lab/service.py
test -e /root/work/test-lifespan-cleanup-lab/test_service.py || cp /opt/fixtures/ten_labs/test-lifespan-cleanup-lab/test_service.py /root/work/test-lifespan-cleanup-lab/test_service.py
cd /root/work/test-lifespan-cleanup-lab

変更可能なリストを、グローバルやデフォルト引数で共有しません。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。

保存後、bash /opt/lab/checks/test-lifespan-cleanup-lab/01-contract.shで確認してください。

二重の開始を拒否する(テスト)

/root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: start(resource)は、すでに開いていればValueError、そうでなければopen=Trueに変えてeventsに'open'を追加します。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

2回開始してリソースを1つ見失う動作を拒否します。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。

保存後、bash /opt/lab/checks/test-lifespan-cleanup-lab/02-contract.shで確認してください。

終了を冪等にする(テスト)

/root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: stop(resource)は、開いているときだけopen=Falseに変え、'close'をeventsに追加します。すでに閉じていればそのままにします。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

複数の後始末の経路が重なっても、重複したcloseイベントが出てはいけません。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。

保存後、bash /opt/lab/checks/test-lifespan-cleanup-lab/03-contract.shで確認してください。

閉じたリソースの使用を防ぐ(テスト)

/root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: read(resource)は、閉じていればRuntimeError、開いていれば{ready:True}を返します。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

準備ができた状態と、オブジェクトが存在することは別です。オブジェクトがあっても、閉じていることがあります。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。

保存後、bash /opt/lab/checks/test-lifespan-cleanup-lab/04-contract.shで確認してください。

例外の経路にfinallyを置く(テスト)

/root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: scope(resource)はcontextmanagerです。入るときにstartし、ブロックの中にはresourceをyieldし、ブロックの成功・失敗のどちらでもstopで閉じます。ブロックの例外は伝播させます。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

yieldの後にだけcloseを書くと、例外が発生した場合にその行へ到達できません。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。

保存後、bash /opt/lab/checks/test-lifespan-cleanup-lab/05-contract.shで確認してください。

アプリのライフサイクルとリソースをつなぐ(テスト)

/root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: lifespan_for(resource)は、asynccontextmanagerの関数lifespan(app)を返します。scope(resource)の中でapp.state.resourceを設定してyieldします。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

lifespan関数自体を呼び出すのではなく、FastAPIのコンストラクターに渡します。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。

保存後、bash /opt/lab/checks/test-lifespan-cleanup-lab/06-contract.shで確認してください。

準備状態を実際のリクエストで読み取る(テスト)

/root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: create_app(resource)は、lifespan_forを使います。GET /readyは、app.state.resourceをreadした結果を返します。contextの終了時にリソースを閉じる必要があります。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

with TestClientを使って初めて、lifespanの開始と終了の両方が実行されます。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。

保存後、bash /opt/lab/checks/test-lifespan-cleanup-lab/07-contract.shで確認してください。

リクエストの後の失敗も後始末する(テスト)

/root/work/test-lifespan-cleanup-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: exercise(resource, fail=False)は、with TestClient(create_app(resource))の中でGET /readyを呼び出します。fail=Trueならその中でRuntimeErrorを送出し、そうでなければレスポンスのJSONを返します。どちらの場合もリソースが閉じている必要があります。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

正常な経路と例外の経路を、同じ後始末の構造にまとめれば、抜けている終了経路を減らせます。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。

保存後、bash /opt/lab/checks/test-lifespan-cleanup-lab/08-contract.shで確認してください。