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

テストツール実戦

sleepを使わず時刻境界を再現する

TT Labで続きを見る

目標

フェイククロックで、制限ウィンドウ・リトライ時刻・ユーザーごとの分離を検証します。

なぜ重要なのか

リクエスト制限のテストにsleepを入れたところ、開発PCでは成功し、CIでは失敗しました。遅い実行環境が時間の境界を変え、テスト自体も長くかかりました。時間はプログラムの入力なので、呼び出し側が制御できるようにし、境界を正確に踏む必要があります。

ステップ

  1. /root/work/test-clock-window-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: validate_limit(limit, window)は、boolを除く正のintのlimitと、正で有限のint/floatのwindowだけを許可し、(limit, float(window))を返します。それ以外はValueErrorです。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

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

mkdir -p /root/work/test-clock-window-lab
test -e /root/work/test-clock-window-lab/service.py || cp /opt/fixtures/ten_labs/test-clock-window-lab/service.py /root/work/test-clock-window-lab/service.py
test -e /root/work/test-clock-window-lab/test_service.py || cp /opt/fixtures/ten_labs/test-clock-window-lab/test_service.py /root/work/test-clock-window-lab/test_service.py
cd /root/work/test-clock-window-lab
  1. /root/work/test-clock-window-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: active(history, now, window)は、now-windowより大きい時刻だけを、元の順序の新しいリストとして返します。historyはソート済みの非減少の時刻です。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

  2. /root/work/test-clock-window-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: retry_after(history, now, window)は、すでに整理された空でないhistoryの最初の時刻+window-nowをceilした値と、0のうち、大きいほうの整数です。空のリストは0です。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

  3. /root/work/test-clock-window-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: history_for(state, key)は、存在しないキーなら空のリスト、存在すればその記録のコピーを返します。参照しただけではstateを変更しません。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

  4. /root/work/test-clock-window-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: admit(state, key, now, limit, window)は、設定を検証した後、該当するキーの期限切れの記録を整理します。余裕があればnowを追加して(True,0)を返し、満杯なら追加せずに(False,retry_after)を返します。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

  5. /root/work/test-clock-window-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: client_key(value)は、1–40文字のASCIIの英数字・ハイフンの文字列をそのまま返し、それ以外はValueErrorです。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

  6. /root/work/test-clock-window-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: limited_response(wait)は、ステータス429、本文{error:'rate_limited'}、Retry-Afterヘッダーはwaitを文字列にした値のJSONResponseです。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

  7. /root/work/test-clock-window-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: create_app(clock, limit=2, window=10)は、GET /workでX-Client-IDを検査し、不正なキーは400 {error:'invalid_client'}、許可は200 {ok:True}、超過はlimited_responseです。stateはアプリごとに分けます。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

参考

設定を検証する(テスト)

/root/work/test-clock-window-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: validate_limit(limit, window)は、boolを除く正のintのlimitと、正で有限のint/floatのwindowだけを許可し、(limit, float(window))を返します。それ以外はValueErrorです。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

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

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

boolはintのサブタイプです。NaNと無限大も、別に拒否する必要があります。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。

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

ウィンドウの左端の境界を除外する(テスト)

/root/work/test-clock-window-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: active(history, now, window)は、now-windowより大きい時刻だけを、元の順序の新しいリストとして返します。historyはソート済みの非減少の時刻です。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

ちょうど期限切れになった時刻を残す>=と、>の違いを確認します。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。

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

待機時間を切り上げる(テスト)

/root/work/test-clock-window-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: retry_after(history, now, window)は、すでに整理された空でないhistoryの最初の時刻+window-nowをceilした値と、0のうち、大きいほうの整数です。空のリストは0です。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

0.2秒残っているからといってRetry-Afterを0にすると、クライアントがすぐに再リクエストします。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。

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

キーごとに記録を分ける(テスト)

/root/work/test-clock-window-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: history_for(state, key)は、存在しないキーなら空のリスト、存在すればその記録のコピーを返します。参照しただけではstateを変更しません。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

共有されたリストを返すと、あるリクエストの整理が、別のリクエストの記録を変えてしまうことがあります。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。

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

許可したリクエストだけを記録する(テスト)

/root/work/test-clock-window-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: admit(state, key, now, limit, window)は、設定を検証した後、該当するキーの期限切れの記録を整理します。余裕があればnowを追加して(True,0)を返し、満杯なら追加せずに(False,retry_after)を返します。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

拒否したリクエストを追加すると、リトライするたびに有効期限が後ろへずれていきます。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。

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

クライアントキーを検証する(テスト)

/root/work/test-clock-window-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: client_key(value)は、1–40文字のASCIIの英数字・ハイフンの文字列をそのまま返し、それ以外はValueErrorです。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

無制限のキーサイズで状態のメモリを圧迫しないように、入力の範囲を制限します。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。

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

拒否レスポンスを作る(テスト)

/root/work/test-clock-window-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: limited_response(wait)は、ステータス429、本文{error:'rate_limited'}、Retry-Afterヘッダーはwaitを文字列にした値のJSONResponseです。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

クライアントがリトライする時間を知れるように、ステータスとヘッダーを一緒に送ります。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。

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

仮想時間でリクエストの流れを完結させる(テスト)

/root/work/test-clock-window-lab/test_service.pyで、提供されたservice.pyの次の公開契約をテストしてください: create_app(clock, limit=2, window=10)は、GET /workでX-Client-IDを検査し、不正なキーは400 {error:'invalid_client'}、許可は200 {ok:True}、超過はlimited_responseです。stateはアプリごとに分けます。正常な実装では成功し、この契約に違反する実装では、実際のテスト本文の失敗として検出する必要があります。前のステップのテストを維持したまま、test_関数を追加してください。

実際にsleepせず、リストに入れた現在時刻をclock関数で渡します。実装ファイルは修正しません。pytest.raisesで期待する例外を確認し、正常な結果には具体的な期待値をassertしてください。

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