エラー応答の回帰を防ぐ契約テスト
目標
既知のエラー・未知のエラー・非公開情報・追跡ヘッダーをテストします。
なぜ重要なのか
エラーメッセージを変更した後、モバイルアプリのリトライのロジックが壊れました。テストは例外が発生したという事実しか確認しておらず、ステータスと公開本文の意味は比較していませんでした。内部の例外がユーザーにそのまま露出するリグレッションも、同じ隙間をすり抜けました。
ステップ
/root/work/test-error-contract-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: DomainError(code, message)はExceptionのサブクラスで、.codeにcodeを保持します。str(例外)はmessageです。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。
最初に1回だけ準備してください。既存のファイルは上書きしません。
mkdir -p /root/work/test-error-contract-lab
test -e /root/work/test-error-contract-lab/service.py || cp /opt/fixtures/ten_labs/test-error-contract-lab/service.py /root/work/test-error-contract-lab/service.py
test -e /root/work/test-error-contract-lab/test_service.py || cp /opt/fixtures/ten_labs/test-error-contract-lab/test_service.py /root/work/test-error-contract-lab/test_service.py
cd /root/work/test-error-contract-lab
-
/root/work/test-error-contract-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: status_for(code)は、missing=404、conflict=409、invalid=422、それ以外=500です。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。 -
/root/work/test-error-contract-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: public_message(code)は、missing='Resource not found'、conflict='State conflict'、invalid='Invalid request'、それ以外='Internal error'です。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。 -
/root/work/test-error-contract-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: request_id(value)は、ASCIIの英数字・アンダースコア・ハイフンだけで構成された1–32文字の文字列ならそのまま、そうでなければ'untracked'です。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。 -
/root/work/test-error-contract-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: problem(code, rid)は、type='urn:labhub:problem:'+code、titleとdetail=public_message(code)、status=status_for(code)、request_id=request_id(rid)だけを持つ辞書です。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。 -
/root/work/test-error-contract-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: response_for(code, rid)は、problemを本文に、status_forをステータスに、application/problem+jsonをmedia_typeに、X-Request-IDを正規化したridにしたJSONResponseです。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。 -
/root/work/test-error-contract-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: install_handlers(app)は、DomainErrorのハンドラーを登録します。ヘッダーX-Request-IDを読み取り、exc.codeについてresponse_forを返します。excのmessageはレスポンスに入れません。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。 -
/root/work/test-error-contract-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: create_app()は、ハンドラーをインストールし、GET /fail/{code}でDomainError(code, 内部文言)を送出します。ただしcode=boomならRuntimeErrorを送出します。RuntimeErrorのハンドラーは、code=internalの固定の500問題レスポンスを返します。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。
参考
- インターネットやパッケージのインストールなしで、既存のlab-dev環境で行います。
- 各ステップは、45秒の採点バジェットの範囲内で実行されます。実際のsleepやネットワーク呼び出しを追加しないでください。
- 提出したテストは、別の一時フォルダーで、正常な実装と欠陥のある実装に対して実行されます。正常な実装では、実際に実行したテストがすべて成功し、欠陥のある実装では、テスト本文が失敗する必要があります。収集エラー、実行0件、すべてスキップ、強制終了は成功ではありません。pytestの基本機能と提供されたライブラリだけを使ってください。
- FastAPI公式ドキュメント・pytest公式ドキュメント・Python sqlite3
- 制限: このラボの問題レスポンスは、type・title・status・detail・request_idを持つ学習用の契約です。汎用的な国際化や、標準への完全な適合は主張しません。request idは追跡に使う文字列であって認証の手段ではなく、本番のログにも機密値をそのまま記録してはいけません。提供された実装は読んでもかまいませんが、採点は別のコピーを使用します。ソースの文言の検査やファイルの修正で欠陥を回避せず、公開インターフェースの実行結果を検査してください。
業務例外にcodeを残す(テスト)
/root/work/test-error-contract-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: DomainError(code, message)はExceptionのサブクラスで、.codeにcodeを保持します。str(例外)はmessageです。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。
最初に1回だけ準備してください。既存のファイルは上書きしません。
mkdir -p /root/work/test-error-contract-lab
test -e /root/work/test-error-contract-lab/service.py || cp /opt/fixtures/ten_labs/test-error-contract-lab/service.py /root/work/test-error-contract-lab/service.py
test -e /root/work/test-error-contract-lab/test_service.py || cp /opt/fixtures/ten_labs/test-error-contract-lab/test_service.py /root/work/test-error-contract-lab/test_service.py
cd /root/work/test-error-contract-lab
機械が判断するcodeと、人が見る内部メッセージを分けます。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。
保存後、bash /opt/lab/checks/test-error-contract-lab/01-contract.shで確認してください。
codeをステータスにマッピングする(テスト)
/root/work/test-error-contract-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: status_for(code)は、missing=404、conflict=409、invalid=422、それ以外=500です。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。
未知のcodeを成功として扱いません。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。
保存後、bash /opt/lab/checks/test-error-contract-lab/02-contract.shで確認してください。
公開する文言を固定する(テスト)
/root/work/test-error-contract-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: public_message(code)は、missing='Resource not found'、conflict='State conflict'、invalid='Invalid request'、それ以外='Internal error'です。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。
例外の文字列にDBのアドレスや内部パスが含まれていても、公開しません。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。
保存後、bash /opt/lab/checks/test-error-contract-lab/03-contract.shで確認してください。
リクエストidを制限する(テスト)
/root/work/test-error-contract-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: request_id(value)は、ASCIIの英数字・アンダースコア・ハイフンだけで構成された1–32文字の文字列ならそのまま、そうでなければ'untracked'です。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。
任意のヘッダーを反射しないように、長さと文字集合を一緒に制限します。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。
保存後、bash /opt/lab/checks/test-error-contract-lab/04-contract.shで確認してください。
問題本文を作る(テスト)
/root/work/test-error-contract-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: problem(code, rid)は、type='urn:labhub:problem:'+code、titleとdetail=public_message(code)、status=status_for(code)、request_id=request_id(rid)だけを持つ辞書です。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。
本文とHTTPステータスが食い違っていると、クライアントはどちらの値を信じるべきか判断できません。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。
保存後、bash /opt/lab/checks/test-error-contract-lab/05-contract.shで確認してください。
レスポンス形式を一貫させる(テスト)
/root/work/test-error-contract-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: response_for(code, rid)は、problemを本文に、status_forをステータスに、application/problem+jsonをmedia_typeに、X-Request-IDを正規化したridにしたJSONResponseです。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。
単純な辞書を返すだけでは、エラーも200になってしまうことがあります。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。
保存後、bash /opt/lab/checks/test-error-contract-lab/06-contract.shで確認してください。
業務例外のハンドラーをつなぐ(テスト)
/root/work/test-error-contract-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: install_handlers(app)は、DomainErrorのハンドラーを登録します。ヘッダーX-Request-IDを読み取り、exc.codeについてresponse_forを返します。excのmessageはレスポンスに入れません。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。
例外を捕捉する場所を散らさず、アプリの共通の境界に置きます。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。
保存後、bash /opt/lab/checks/test-error-contract-lab/07-contract.shで確認してください。
予期しないエラーも隠す(テスト)
/root/work/test-error-contract-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: create_app()は、ハンドラーをインストールし、GET /fail/{code}でDomainError(code, 内部文言)を送出します。ただしcode=boomならRuntimeErrorを送出します。RuntimeErrorのハンドラーは、code=internalの固定の500問題レスポンスを返します。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。
テストでは例外の再送出を無効にして、実際の500レスポンスのバイト列を確認します。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。
保存後、bash /opt/lab/checks/test-error-contract-lab/08-contract.shで確認してください。