設定テストのグローバル状態を分離する:設計原理
一言でいうと
誤った環境値・デフォルト値・スナップショット・機密フィールドの露出というリグレッションを検証します。
なぜ必要なのか
先に実行したテストが環境変数を変更したために、後のテストが失敗しました。テストの順序を固定すると問題は隠れましたが、実際のアプリで設定の打ち間違いまで正常なデフォルト値に変わってしまうバグは残りました。入力の分離と、厳密なパースを別々に確認する必要があります。
どう動くのか
各テストに新しい環境dictを渡します。環境変数がない場合と、存在するが誤っている場合を分け、bool文字列、ポートの範囲、有限のtimeoutを確認します。アプリの生成後に元の入力を変更して、スナップショットが独立しているかを検査し、公開レスポンスのキーの集合をアサートします。
학생 테스트 → 정상 구현: 실제 시험 모두 통과
└→ 계약 위반 구현: 해당 동작에서 실패
수집 실패·0개 실행·강제 종료 ≠ 결함 검출
契約を読んで失敗を予測するワークシート
以下は、実装を丸ごと暗記するための答案ではなく、ステップごとのコードレビューです。各変更の断片は、意図的に契約を破っています。変更後でも、正常なケースが成功することがある点に注意してください。実行する前に、どの入力・例外・状態を観測すれば違いが表れるかを予想し、実装した後で、その予想と結果を比較します。
1. ブール値を明示的にパースする(テスト)
提供されたservice.pyの次の公開契約をテストしてください: parse_bool(value)は、大文字小文字を区別しないtrueまたはfalseだけをboolとして返します。空白が付いている場合や文字列でない場合はValueErrorです。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。
判断の根拠: bool('false')はTrueです。許可する2つの文字列を直接比較してください。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。
レビューする誤った変更の断片:
bool(value)
この断片が入った関数の公開契約と比較してください。成功するケースが1つだけでは区別できない場合は、拒否されるべき入力や、失敗した後の状態を観測の対象に選びます。
2. ポートの範囲を検査する(テスト)
提供されたservice.pyの次の公開契約をテストしてください: parse_port(value)は、ASCIIの数字だけの文字列をintに変換し、1–65535なら返します。それ以外はValueErrorです。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。
判断の根拠: 整数への変換に成功しても、有効なポートの範囲だという意味ではありません。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。
レビューする誤った変更の断片:
<= 65536
この断片が入った関数の公開契約と比較してください。成功するケースが1つだけでは区別できない場合は、拒否されるべき入力や、失敗した後の状態を観測の対象に選びます。
3. 時間制限を有限の値にする(テスト)
提供されたservice.pyの次の公開契約をテストしてください: parse_timeout(value)は、文字列をfloatに変換し、0より大きく30以下の有限の値だけを返します。それ以外はValueErrorです。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。
判断の根拠: NaNは通常の比較で期待と異なる動作をするので、isfiniteを確認します。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。
レビューする誤った変更の断片:
0 <= number <= 30
この断片が入った関数の公開契約と比較してください。成功するケースが1つだけでは区別できない場合は、拒否されるべき入力や、失敗した後の状態を観測の対象に選びます。
4. 必須の機密値の欠落を拒否する(テスト)
提供されたservice.pyの次の公開契約をテストしてください: required_token(env)は、TOKENが文字列で、stripした後に空でないとき、stripした値を返します。存在しない、または空の値はValueErrorです。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。
判断の根拠: 欠落した必須の機密値を、例のデフォルト値で置き換えません。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。
レビューする誤った変更の断片:
return value
この断片が入った関数の公開契約と比較してください。成功するケースが1つだけでは区別できない場合は、拒否されるべき入力や、失敗した後の状態を観測の対象に選びます。
5. 欠落したときだけデフォルトを適用する(テスト)
提供されたservice.pyの次の公開契約をテストしてください: load_settings(env)は、service=envのSERVICE(欠落時は'api')、debug=parse_bool(DEBUG、欠落時は'false')、port=parse_port(PORT、欠落時は'8000')、timeout=parse_timeout(TIMEOUT、欠落時は'5')、token=required_tokenの辞書です。空のSERVICEはValueErrorです。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。
判断の根拠: getのデフォルト値と、or演算子によるデフォルト値への置き換えは、空文字列の扱いが異なります。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。
レビューする誤った変更の断片:
env.get("DEBUG","true")
この断片が入った関数の公開契約と比較してください。成功するケースが1つだけでは区別できない場合は、拒否されるべき入力や、失敗した後の状態を観測の対象に選びます。
6. 公開する設定から機密値を除去する(テスト)
提供されたservice.pyの次の公開契約をテストしてください: public_settings(settings)は、serviceとdebugだけを持つ新しい辞書です。元のデータは変更しません。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。
判断の根拠: トークンの値の一部をマスクするよりも、フィールド自体を公開しない契約を使います。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。
レビューする誤った変更の断片:
"debug":settings["debug"],"token":settings["token"]}
この断片が入った関数の公開契約と比較してください。成功するケースが1つだけでは区別できない場合は、拒否されるべき入力や、失敗した後の状態を観測の対象に選びます。
7. 外部の変更と設定を分離する(テスト)
提供されたservice.pyの次の公開契約をテストしてください: snapshot(env)は、load_settingsの結果を返します。呼び出した後でenvを変更しても、返された設定は変わりません。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。
判断の根拠: アプリの起動時点の設定を、あとで変わる入力の辞書から切り離します。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。
レビューする誤った変更の断片:
return env
この断片が入った関数の公開契約と比較してください。成功するケースが1つだけでは区別できない場合は、拒否されるべき入力や、失敗した後の状態を観測の対象に選びます。
8. 起動の失敗と公開レスポンスを確認する(テスト)
提供されたservice.pyの次の公開契約をテストしてください: create_app(env)は、snapshotをただちに読み取り、誤った設定ならValueErrorでアプリの生成を失敗させます。GET /infoはpublic_settingsだけを返します。別のenvで作ったアプリ同士は、設定を共有しません。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。
判断の根拠: サーバーが起動した後の最初のリクエストで設定エラーが発覚することがないように、生成時に検証します。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。
レビューする誤った変更の断片:
return settings
この断片が入った関数の公開契約と比較してください。成功するケースが1つだけでは区別できない場合は、拒否されるべき入力や、失敗した後の状態を観測の対象に選びます。
現場での姿
通常の辞書で環境を注入するので、実際のプロセスのグローバルな環境には依存しません。暗号化された機密情報ストア、キーのローテーション、動的な再読み込みまで実装する例ではありません。トークンの文字列は学習用の入力であり、実際の本番のキーをラボのPodに入れることはありません。提供された実装は読んでもかまいませんが、採点は別のコピーを使用します。ソースの文言の検査やファイルの修正で欠陥を回避せず、公開インターフェースの実行結果を検査してください。
次のラボですること
8つのステップが、1つの実行可能な成果物につながります。ブール値を明示的にパースする(テスト) → ポートの範囲を検査する(テスト) → 時間制限を有限の値にする(テスト) → 必須の機密値の欠落を拒否する(テスト) → 欠落したときだけデフォルトを適用する(テスト) → 公開する設定から機密値を除去する(テスト) → 外部の変更と設定を分離する(テスト) → 起動の失敗と公開レスポンスを確認する(テスト)。
各ステップでは、関数やファイルが存在するという事実ではなく、実際の戻り値・例外・状態の変化を検査します。正解を見たあとは、わざと境界の比較や後始末のコードを変えて、どのテストが失敗するかを確認してください。前のテストが次のステップでも維持される理由を説明し、このラボが保証しない本番の条件を1つ書いてみてください。