Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】Four bugs my test suite couldn't catch

2026年09月21日に「Dev.to」が公開したITニュース「Four bugs my test suite couldn't catch」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

オフライン配信機能開発で、テストは合格したが、本番では動かなかった。非同期処理やデータ損失など4つのバグは、部品間の連携不足が原因だった。個々のテストでは見つけにくいシステム全体の不具合は、実際の環境で動かす検証が重要だと分かった。

出典: Four bugs my test suite couldn't catch | Dev.to公開日:

ITニュース解説

このニュース記事は、システムエンジニアが開発中に直面する、特にテストでは見つけにくいバグとその対処法について重要な学びを提供している。筆者は、エンドツーエンドで暗号化されたメッセンジャーアプリを開発しており、その中で、相手がオフラインの時にメッセージを一時的に保存し、オンラインになったら届ける「オフライン配信機能」を追加した。この機能開発では、単体テスト、結合テスト、エンドツーエンドテストといった多岐にわたるテストを実施し、すべて「成功」という結果を得ていた。しかし、実際にデプロイされたアプリを動かしてみると、メッセージが全く届かないという致命的な問題が発覚したのだ。ここから、テストでは捉えられなかった四つのバグと、それらから得られた貴重な教訓が語られる。

一つ目のバグは、テスト環境では発生しない「競合状態」だった。メッセンジャーアプリは、ユーザーが起動すると、まずサーバーに接続する。その直後、過去のメッセージを復号化するための暗号キーをブラウザのストレージから読み込むのだが、このキーの読み込みは時間がかかる非同期処理である。問題は、サーバーが接続とほぼ同時に、オフライン中に届いたメッセージをクライアントに送ってしまったことだ。クライアント側ではまだ暗号キーの準備ができていないため、届いたメッセージは解読できず、そのまま破棄されてしまった。テスト環境では、キーの読み込みが非常に高速だったため、接続とキー準備の間に時間差が生じず、この競合状態は検出されなかった。実際の環境では、データ読み込みなどの入出力処理には時間がかかり、この「隙間時間」が発生しうる。ここでの教訓は、テスト環境と本番環境で、非同期処理や入出力処理の速度が異なる場合があることを意識する必要がある点だ。特に、接続が完了してからシステムが「完全に準備が整った」と判断するまでの間に、データが届く可能性を考慮に入れるべきである。この問題は、暗号キーの読み込みをサーバー接続より先に行うよう、処理の順序を変更することで解決された。

二つ目のバグは「誤ったタイミングでの確認応答」だった。サーバーは、クライアントがメッセージを「受け取った」ことを確認すると、そのメッセージを自身の保存領域から削除する仕組みだった。しかし、クライアント側が「メッセージを受信した」と判断するタイミングが早すぎたのだ。メッセージがネットワーク経由でクライアントに「到達した」時点で確認応答を送ってしまっていたため、サーバーはメッセージを削除した。だが実際には、クライアント側でそのメッセージが暗号化を解除されていなかったり、アプリ内に保存できていなかったり、ユーザー画面に表示できていなかったりすることがあった。つまり、アプリがメッセージを「適切に処理できた」わけではないのに、サーバーからメッセージが消えてしまい、結果としてユーザーには何も届かず、メッセージは完全に消失した。このバグは、データ消失という重大な結果を招いた。この教訓は、「メッセージを受信した」ことと「メッセージを適切に処理できた」ことは全く異なるイベントであり、特にデータの信頼性に関わる処理では、メッセージが実際に処理され、安全に保管されたと確認できてから初めて、サーバーに削除を依頼すべきであるということだ。この問題は、メッセージがアプリ内で完全に処理されたことを確認してから、サーバーに確認応答を送るように変更することで解決された。

三つ目のバグは「実行されないクリーンアップ処理」だった。このオフライン配信機能の設計では、相手がオフラインの場合にメッセージをデータベースに一時保存し、相手がオンラインになったらそのメッセージを削除するはずだった。しかし、実際には、常に相手がオンラインでメッセージが直接配信された場合も、メッセージがデータベースに一時的に保存されていたのだ。そして、直接配信されたメッセージは、受信者からの確認応答が発生しないため、データベースから削除されることがなかった。その結果、活発にやり取りされるチャットの履歴が、本来意図しない形でサーバーのデータベースに蓄積され続け、週に一度の定期的なクリーンアップでしか消去されない状態になっていた。これはプライバシーの観点からも大きな問題となる。この教訓は、何かデータを削除する処理を設計した場合、その削除処理が「全ての」発生しうるコードの経路において確実に実行されるかを確認する必要があるという点だ。特定の条件でのみクリーンアップが実行される設計は、意図しないデータ残存や、データプライバシーに関わる問題を引き起こす可能性がある。この問題は、相手が不在の場合にのみメッセージをサーバーに保存するように変更することで解決された。

四つ目のバグは「オーナーを失った状態データの残存」だった。このバグは、ユーザーがチャットを削除した際に発生した。チャットを削除すると、メッセージ自体は消去されるものの、そのチャットに使われていた暗号化キーがアプリ内に残ってしまったのだ。その後、同じ相手ともう一度チャットを始めると、アプリは残っていた古い暗号化キーを使って通信しようとする。しかし、相手側はすでに古いチャットと関連キーを削除しているため、両者の間でキーが合致せず、結果としてメッセージが全く復号化できなくなった。ここでの教訓は、あるデータや機能を削除する際には、それに「関連する全ての派生データ」も一緒に削除することが極めて重要であるという点だ。残された古いデータは無害に見えるかもしれないが、後になってそのデータがまだ有効だと誤解したコードによって利用され、システム全体の不整合や機能不全を引き起こす原因となる。この問題は、チャット削除時に、そのチャットに関連する暗号化キーも確実に削除するように変更することで解決された。

これらのバグから筆者が得た最も重要な教訓は、「テストをさらに増やすこと」だけでは不十分だという点だ。多くのテストが書かれ、すべてパスしていたにも関わらず、機能は完全に動作しなかった。これらのバグに共通していたのは、いずれも異なるコンポーネント(システムを構成する個々の部品)間の「隙間」や「境界」で発生していた点にある。例えば、サーバー接続と暗号キー読み込みの間、サーバーが考える「配信済み」とクライアントが考える「処理済み」の間、直接配信と一時保存という異なるメッセージ配信経路の間、といった具合だ。それぞれのコンポーネントは単独では正しく動作していたが、それらが組み合わさったシステム全体としては、意図したように機能しなかったのである。

この経験に基づき、筆者は新たなルールを導入した。それは、実際のストレージ、実際のネットワーク、または実際の別のマシンとやり取りするような処理は、それが「完了した」と判断する前に、必ず「実際にデプロイされた本番環境」で検証するというものだ。開発用サーバーやテスト環境だけでなく、実際にユーザーが使うであろう「実機」を用い、ユーザーが行うであろう操作を実際に試すのだ。今回のケースでは、このたった一度の「実機テスト」によって、ユーザーデータを失う可能性のある二つのバグを含む、四つの深刻なバグがわずか10分程度の時間で見つかったという。

テストが全て成功(グリーン)表示であっても、それは「個々の部品が仕様通りに動く」ことを示すに過ぎない。システム全体が意図した通りに機能するかどうかは、実際の利用状況、つまり本番環境での検証を通じてしか確実に確認できないことが多い。この経験は、システム開発におけるテストの重要性を否定するものではなく、むしろテストの限界と、実際のユーザー体験を模倣した本番環境での検証の必要性を強く示唆している。

関連コンテンツ

関連IT用語