【ITニュース解説】Mocking Timeouts: When a Stub Never Returns and the Client Retries
2026年11月03日に「Dev.to」が公開したITニュース「Mocking Timeouts: When a Stub Never Returns and the Client Retries」について初心者にもわかりやすく解説しています。
ITニュース概要
クライアントのタイムアウトやリトライ処理をテストするには、成功応答だけでなく、サーバーが意図的に応答を遅延させる、または応答しないモックが不可欠だ。これにより、クライアントがタイムアウトでエラーを適切に処理したり、リトライや冪等性キーを正しく利用するか検証できる。
ITニュース解説
システム開発では、アプリケーションが常にスムーズに動作する「ハッピーパス」と呼ばれる成功ケースだけでなく、サーバーが応答しなかったり、応答が遅れたりするような失敗ケースをテストすることが非常に重要だ。なぜなら、このような状況にクライアントアプリケーションがどう対応するかを事前に検証しておかなければ、実際の運用で予期せぬ問題、例えばアプリケーションが永久に待ち続けたり、サーバーに無数のリクエストを送りつけたり、あるいは同じ注文を二度生成してしまったりする事態に陥る可能性があるからだ。このような問題は、クライアントアプリケーションが持つ「タイムアウト」や「リトライ(再試行)」、「バックオフ(再試行間隔の延長)」、「冪等性(同じ操作を複数回実行しても結果が変わらないこと)」といった、失敗時の挙動を管理するロジックが正しく機能しない場合に発生する。これらのロジックはクライアント側に実装されており、本物のサーバーの代わりをする「モック」を使って、意図的に失敗状況を作り出すことで検証できる。
しかし、常に「成功しました」(HTTP 200 OK)と返すだけのモックでは、このような失敗ケースをテストできない。クライアントのタイムアウトやリトライのロジックが本当に正しく動作するかを確認するには、モックが意図的に応答を遅らせたり、全く応答を返さなかったりする必要がある。モックを使ってテストすべき失敗のシナリオは主に二つ存在する。一つは「ハードタイムアウト」だ。これは、モックがクライアントからのリクエストに対して一切応答を返さず、通信路を開いたままにするケースを指す。この場合、クライアントは設定された待ち時間を過ぎたら、自ら通信を打ち切り、タイムアウトによるエラーを発生させるべきだ。もう一つは「スローレスポンス」だ。これは、モックが応答を返すものの、その応答がクライアントのタイムアウト期限を過ぎてから届くケースだ。クライアントは既にタイムアウトして処理を中止しているはずなので、遅れて届いたこの応答は安全に無視し、アプリケーションの状態に影響を与えないようにする必要がある。この二つのケースを混同すると、「リクエストはタイムアウトしたのに、サーバー上では処理が完了していた」というような不整合が起こり、例えばユーザーが注文を再試行した結果、同じ商品が二重に注文されるといった問題につながる。
これらの状況をモックで再現するには、多くのモックサーバーが持っている「遅延」を設定する機能を利用する。この遅延時間は、クライアントが設定しているタイムアウト時間を必ず超えるように設定することが重要だ。そうすることで、クライアント側で実際にタイムアウト処理が実行されることを確実に検証できる。また、テストをより早く、かつ確実に実行するために、クライアント側のタイムアウト設定も一時的に短く調整することが推奨される。例えば、JavaScriptのfetch関数などでAbortControllerのような機能を使って、一定時間後にリクエストを強制的に中止させるように設定できる。これにより、モックが応答しない場合でも、クライアントは設定した時間で確実に処理を中断し、タイムアウトエラーを発生させることができる。
クライアントがリクエストを再試行する際の挙動をテストすることも欠かせない。リトライテストでは、再試行が何回行われたか、それぞれの再試行の間に適切な間隔が空いているか(バックオフが機能しているか)、そしてすべての再試行で同じ「冪等性キー」が送られているか、という三つの点を確認する必要がある。この冪等性キーがないと、実際のサーバーに対してリトライが実行された際に、ユーザーに二重の請求が発生するような予期せぬ問題を引き起こす可能性がある。再試行の間隔を検証する際、実際の時間を待つとテストが非常に遅くなってしまうため、テストフレームワークの機能を使って「時間」を注入し、時間をスキップすることで、テストの実行速度を保ちつつ、バックオフのロジックが正しく機能しているかを検証できる。
テストを行う上で避けるべきいくつかの点がある。まず、テスト中にクライアントのタイムアウト設定を無限にしたり、非常に長く設定したりしてはいけない。それでは、テストしたい「タイムアウト時の挙動」が隠されてしまい、テストが無意味になる。次に、クライアントが持つ「リトライ機能そのもの」をモック化することも避けるべきだ。テストの目的は、実際のネットワーク通信を伴うリトライの仕組み全体が正しく動作するかを確認することなので、テスト対象の機能をモックしてしまうと、そのテストは意味をなさなくなる。また、モックに遅延設定をした場合、テストが終わるごとにその設定をリセットすることを忘れてはならない。リセットしないと、その後のテストが前のテストの遅延設定を引き継いでしまい、原因不明のエラーを引き起こす可能性がある。さらに、HTTP 503エラー(サービス利用不可)とタイムアウトを同じリトライケースとして扱うのも間違いだ。503エラーはサーバーからの明確な応答であり、「再試行までの推奨時間」(Retry-Afterヘッダー)が含まれる場合があるが、タイムアウトはサーバーからの応答がない状態であり、そのようなシグナルは存在しない。それぞれ異なるシナリオとして扱うべきだ。
もう一つ、重要な区別として「コネクションタイムアウト」と「リードタイムアウト」がある。これらはクライアントが個別に設定することが多い。コネクションタイムアウトは、クライアントがサーバーとの通信経路(TCP接続)を確立できない場合に発生する。例えば、指定されたサーバーが存在しない、ネットワーク的に到達できないといった状況だ。一方、リードタイムアウトは、接続は確立されたものの、その後サーバーからデータが全く送られてこない場合に発生する。モックが応答しないように設定するのは、主にこのリードタイムアウトのケースをカバーする。コネクションタイムアウトをテストするには、実際に到達可能だが応答しないサーバーのアドレスを指定したり、通信をブロックするようなポートを指定したりして、クライアントが接続失敗のエラーを正しく報告するかを確認する必要がある。これら二つのタイムアウトでは、クライアントのリトライポリシーが異なることがある。接続失敗の場合はすぐに再試行しても安全なことが多いが、データを書き込むリクエストの途中でリードタイムアウトが発生した場合、サーバー側では既に処理が完了している可能性があるため、再試行の際には必ず冪等性キーを送信する必要がある。
これらのモックテストの真の価値は、クライアントとサーバー間の「失敗時の契約」を明確に定義し、検証することにある。タイムアウト値はコードに直接記述するのではなく、設定ファイルに記述し、本番環境と同じ値を使ってステージング環境でテストを実行することが推奨される。これにより、モックテストでタイムアウトロジックが機能することを確認した上で、ステージング環境でのテストを通じて、本番環境のタイムアウト設定が実際のサーバーの応答時間に対して現実的であるかを確認できる。これらのテストを実践することで、単に機能が動くだけでなく、実際の運用環境で発生しうる困難な状況にも適切に対応できる、信頼性の高いシステムを開発できるだろう。