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

テストツール実戦

エラー応答の回帰を防ぐ契約テスト

TT Labで続きを見る

目標

既知のエラー・未知のエラー・非公開情報・追跡ヘッダーをテストします。

なぜ重要なのか

エラーメッセージを変更した後、モバイルアプリのリトライのロジックが壊れました。テストは例外が発生したという事実しか確認しておらず、ステータスと公開本文の意味は比較していませんでした。内部の例外がユーザーにそのまま露出するリグレッションも、同じ隙間をすり抜けました。

ステップ

  1. /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
  1. /root/work/test-error-contract-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: status_for(code)は、missing=404、conflict=409、invalid=422、それ以外=500です。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

  2. /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_関数を追加してください。

  3. /root/work/test-error-contract-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: request_id(value)は、ASCIIの英数字・アンダースコア・ハイフンだけで構成された1–32文字の文字列ならそのまま、そうでなければ'untracked'です。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

  4. /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_関数を追加してください。

  5. /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_関数を追加してください。

  6. /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_関数を追加してください。

  7. /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_関数を追加してください。

参考

業務例外に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で確認してください。