WebSocketテストは、単純なHTTPリクエスト-レスポンスモデルから持続的接続の検証へと進化しました。初期の自動化は、状態を持つストリームセマンティクスを無視し、ソケットをブラックボックスのHTTPアップグレードとして扱っていました。現代のリアルタイムアプリケーションでは、フレーム上のバイナリプロトコル(Protobufなど)やTCPの劣化下での耐障害性を検証するために、オーダリングの保証が必要です。この質問は、メッセージ消費におけるレースコンディションによりテストが間欠的に失敗する不安定なCIパイプラインを観察する中で生まれました。チームは、本質的に非同期でプッシュベースのアーキテクチャと決定論的なアサーションを調和させることに苦労しました。
コアな課題は、非決定的な待機を導入することなく一時的な特性(順序、レイテンシ)を検証することにあります。WebSocket接続は、ポートの衝突や共有サブスクリプションの汚染を引き起こす並列テスト実行と衝突する状態を持つセッションを維持します。バイナリペイロードは、JSONアサーションとは異なるスキーマ対応の逆シリアライズを必要とし、検証ロジックを複雑にします。ネットワーク耐障害性テストは、アプリケーションコードを変更することなくトランスポート層での障害注入を要求します。伝統的なポーリングベースのSeleniumまたはREST Assuredパターンは、リクエスト-レスポンスサイクルを仮定しており、サーバープッシュストリームには失敗します。
Project ReactorまたはRxJavaを使用して、メッセージストリームを仮想時間機能を持つオブザーバブルシーケンスとしてモデル化する反応的テストハーネスをアーキテクトします。各テストを専用のDockerネットワーク名前空間に隔離しながら、Toxiproxyを使ってネットワーク分断とレイテンシをシミュレートするTestContainersを展開します。各テストがユニークなセッション識別子を生成する相関UUID戦略を実装し、並列ワーカー間でメッセージルーティングの隔離を確保します。バイナリ検証には、アサーションの前にProtobufスキーマに対して逆シリアライズされたByteBufマッチャーやカスタムHamcrestマッチャーを使用します。信号カウントとオーダリングに関する明示的な期待を持ってStepVerifierを使用してテストを実行します。
@Testcontainers public class WebSocketResilienceTest { @Container private static final ToxiproxyContainer toxiproxy = new ToxiproxyContainer("ghcr.io/shopify/toxiproxy:2.5.0"); private WebSocketClient client; private ToxiproxyClient proxyClient; @BeforeEach void setUp() { client = new ReactorNettyWebSocketClient(); proxyClient = new ToxiproxyClient(toxiproxy.getHost(), toxiproxy.getControlPort()); } @Test void shouldMaintainMessageOrderingUnderNetworkLatency() throws IOException { Proxy proxy = proxyClient.createProxy("ws", "0.0.0.0:8666", "host.testcontainers.internal:8080"); proxy.toxics().latency("latency", ToxicDirection.DOWNSTREAM, 2000); StepVerifier.create( client.execute( URI.create("ws://" + toxiproxy.getHost() + ":" + toxiproxy.getMappedPort(8666) + "/stream"), session -> session.receive() .map(WebSocketMessage::getPayloadAsText) .filter(msg -> msg.contains("sequence")) .take(3) .collectList() ) ) .assertNext(messages -> { assertThat(messages) .extracting(json -> JsonPath.read(json, "$.sequence")) .containsExactly(1, 2, 3); }) .verifyComplete(); } }
高頻度取引プラットフォームは、市場データフィードのためのRESTポーリングからWebSocketストリーミングに移行していました。QAチームは、市場のボラティリティスパイクの間にも価格更新が正しい時間順に到着することを検証する必要がありました。この状況で10,000件以上のメッセージ/秒が生成されました。既存のRESTスイートは、WebSocketが数秒で処理できるシナリオを確認するのに8分かかり、オートメーションフレームワークの完全なアーキテクチャの見直しを必要としました。
初期の実装では、Thread.sleep()でメッセージを待機していたため、テストスイートは30秒に達し、40%のフレークリ率がありました。並列実行により、テストは共有されたRedis pub/subバックプレーンから他のメッセージを消費する結果となりました。バイナリProtobufペイロードはBase64文字列として比較され、再現要素の非決定的なフィールド順序により失敗しました。
チームは、メッセージを収集するためにLinkedBlockingQueueを使用し、タイムアウトでポーリングすることを検討しました。これにより単純なアサーションロジックが提供されましたが、非決定的な遅延が導入されました。テストは引き続き遅く、キューの排出におけるレースコンディションが発生し、メッセージが消費よりも早く到着すると間欠的なアサーションエラーが発生しました。このアプローチは、真のオーダリングセマンティクスを検証することもできず、単に最終的な受信を確認するものでした。
WireMockやMockWebServerを使用してキャプチャしたWebSocketフレームを再生することで、決定論的な実行と迅速なフィードバックが提供されました。しかし、TCPパケット損失や再接続ロジックのような真のネットワーク耐障害性問題をキャッチすることはできませんでした。モックは実際のNettyハンドラロジックをアプリケーションサーバーで実行せず、再接続のバグが本番環境に到達することを許しました。
時間をプログラムで操作するためにReactorのTestSchedulerを導入し、Toxiproxyの背後に実際のWebSocketサーバーが稼働するTestContainersを使用することで、ミリ秒単位の高速実行を実現し、実際のネットワーク動作を検証しました。仮想時間スケジューラにより、50ミリ秒未満で5分間のタイムアウトウィンドウをテスト能、し、Toxiproxyは正確なレイテンシと帯域幅制限を注入しました。このアプローチは初期設定に時間がかかりましたが、最も高い忠実性を提供しました。
チームは、実行速度を犠牲にせずに本番環境の忠実性を保持するため、反応的な仮想時間アプローチを選択しました。モックとは異なり、実際のNettyパイプラインと再接続ハンドラを検証しました。ブロッキングキューとは異なり、Fluxシーケンス演算子を通じて決定論的な順序アサーションを提供しました。Dockerネットワークによる隔離により、並列実行の競合が排除されました。
テスト実行時間は、スイートあたり4分から12秒に短縮されました。フレーキネスは、CIの3ヶ月の実行でゼロに減少しました。このフレームワークは、アプリケーションがTCP再接続イベント後にメッセージの重複を排除できなかった重大なバグを発見しましたが、これは以前の手動テストでは見逃されていました。このソリューションは、ポートの競合なしに50の並列CIワーカーにスケーリングしました。
候補者はよく、クライアントインスタンスにsynchronizedブロックやReentrantLockを使用することを提案します。これは実行を直列化し、並列CIの目的に反します。正しいアプローチはアーキテクチャ的な隔離を含みます:各テストクラスは専用のネットワーク名前空間と動的ポート割り当てを持つ独自のTestContainerをインスタンス化します。各テストは、ユニークな相関識別子でタグ付けされたメッセージのみにサブスクライブするため、UUIDベースのルーティングキーを使用します。これにより、パフォーマンスボトルネックなしでゼロの共有状態が保証されます。
ProtobufまたはMessagePackペイロードのBase64エンコーディングには、実装間で変動する可能性のあるワイヤフォーマットのメタデータ(フィールドの順序、未知のフィールドの保持)が含まれます。文字列比較は、意味的に同一のメッセージが異なるバイナリ表現を持つと失敗します。代わりに、公式のProtobufコンパイラを使用して生成されたJava/KotlinクラスにByteBufを逆シリアライズし、その後AssertJの再帰的比較を使用してフィールドごとに深い比較を行います。未知のフィールドについては、プロトの同等性を正確に処理するProtoTruth(GoogleのTruthライブラリ拡張)を使用します。
iptablesやアプリケーションコードの変更にはルート特権が必要で、環境の変動を引き起こします。候補者はよくToxiproxyやPumba(Docker用カオステストツール)を見逃します。これらはテストネットワークにサイドカ containerとして実行され、プログラムによるレイテンシ、帯域幅制限、および接続リセットの注入をREST APIを介して行うことができます。WebSocketクライアントをプロキシエンドポイントを介して接続するように設定します。テスト中に、毒性エンドポイントを呼び出して接続を切断したり100%のパケット損失を引き起こしたりし、その後、クライアントが期待される再接続バックオフ戦略をトリガーし、正しいメッセージシーケンス識別子で再開するかを確認します。