5.2 WebSocket integration test
WebSocket integration test 的核心目標是驗證 client 與 server 透過真實連線互動後,協定行為是否正確。它比單元測試慢,但能覆蓋 HTTP upgrade、read/write pump、router、server message、push flow 與 cleanup。
本章目標
學完本章後,你將能夠:
- 用
httptest.Server建立真實 WebSocket 測試入口 - 將
http://測試 URL 轉成ws:// - 用 deadline 避免 read/write 永久卡住
- 驗證 subscribe、push、error response 與 cleanup
- 分辨 integration test 與 unit test 的責任邊界
【觀察】WebSocket 的錯誤常出現在元件交界
WebSocket 測試的核心困難是很多錯誤不在單一函式裡。Router 單元測試可能通過,但真實連線仍可能因為 upgrade path、read pump、write pump、send buffer 或 unregister 流程出錯。
Integration test 適合驗證這些交界:
- client 能否成功 dial 到
/ws - server 是否接受 client action
- subscribe 後是否收到 acknowledgement
- server broadcast 是否能推到 client
- client 關閉後 hub 是否清理連線
- 錯誤 action 是否回 error message 而不是斷線
這些不是每個單元測試都該覆蓋的內容。Integration test 的價值在於證明多個元件能透過真實協定協作。
【判讀】integration test 補的是協作信心
Integration test 的核心責任是覆蓋協定流程,不是取代所有規則測試。Router validation、topic normalization、dedup key、state transition 應主要用單元測試;WebSocket integration test 只挑關鍵端到端流程。
建議分工:
| 測試類型 | 負責內容 |
|---|---|
| unit test | router、payload validation、subscription state、TrySend |
| integration test | dial、upgrade、read/write pump、server response、cleanup |
| race test | hub、client state、repository 的並發存取 |
如果每個 validation case 都啟動 WebSocket server,測試會變慢且失敗定位不清楚。Integration test 應少量、關鍵、穩定。
【執行】用 httptest.Server 建立真實入口
WebSocket integration test 的核心起點是 httptest.Server。它提供真實 HTTP server,不需要手動管理 port。
1func TestWebSocketSubscribe(t *testing.T) {
2 server := httptest.NewServer(newRouter())
3 t.Cleanup(server.Close)
4
5 wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/ws"
6
7 conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
8 if err != nil {
9 t.Fatalf("dial websocket: %v", err)
10 }
11 t.Cleanup(func() {
12 _ = conn.Close()
13 })
14}httptest.NewServer 產生的是 http://127.0.0.1:port,WebSocket client 需要 ws://127.0.0.1:port/ws,所以常用字串轉換。
若 handler 需要 hub、router、fake repository,應在測試中明確組裝。這讓 integration test 的依賴可控。
【策略】測試 helper 應封裝連線樣板
Integration test 的核心樣板很多:建立 server、轉 URL、dial、設定 cleanup。可以用 helper 降低重複,但不要把協定斷言藏起來。
1func newTestWebSocket(t *testing.T, handler http.Handler) (*websocket.Conn, *httptest.Server) {
2 t.Helper()
3
4 server := httptest.NewServer(handler)
5 t.Cleanup(server.Close)
6
7 wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/ws"
8 conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
9 if err != nil {
10 t.Fatalf("dial websocket: %v", err)
11 }
12 t.Cleanup(func() {
13 _ = conn.Close()
14 })
15
16 return conn, server
17}Helper 負責重複 setup。測試本文仍應清楚寫出「送什麼 message、期待什麼 response」。
【執行】action 測試要檢查協定語意
Action 測試的核心流程是送 client message、讀 server message、檢查協定欄位。
1func TestSubscribeActionReturnsAcknowledgement(t *testing.T) {
2 conn, _ := newTestWebSocket(t, newRouter())
3
4 request := ClientMessage{
5 Action: ActionSubscribeTopic,
6 Data: mustJSON(t, SubscribeTopicRequest{
7 Topic: "alerts",
8 }),
9 }
10
11 if err := conn.WriteJSON(request); err != nil {
12 t.Fatalf("write subscribe: %v", err)
13 }
14
15 response := readServerMessage(t, conn)
16 if response.Type != "topic_subscribed" {
17 t.Fatalf("response type = %q, want topic_subscribed", response.Type)
18 }
19 if response.Topic != "alerts" {
20 t.Fatalf("response topic = %q, want alerts", response.Topic)
21 }
22}這個測試檢查的是協定語意,不只是連線沒有斷。Subscribe 的成功條件是 server 明確回覆訂閱成功。
【執行】每次讀取前設定 deadline
WebSocket integration test 的核心風險是永久卡住。每次等待 server message 前,都應設定 read deadline。
1func readServerMessage(t *testing.T, conn *websocket.Conn) ServerMessage {
2 t.Helper()
3
4 if err := conn.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil {
5 t.Fatalf("set read deadline: %v", err)
6 }
7
8 var response ServerMessage
9 if err := conn.ReadJSON(&response); err != nil {
10 t.Fatalf("read server message: %v", err)
11 }
12 return response
13}Deadline 是測試保護。若 server 沒有送出預期訊息,測試會在合理時間內失敗,而不是卡住整個測試套件。
Timeout 不應過短。CI 可能比本機慢,測試應給足合理緩衝,但仍要能快速暴露失敗。
【執行】推送測試要先建立可觀察觸發點
Server push 的核心測試流程是先讓 client 訂閱 topic,再從 server 端觸發 broadcast,最後讀取 client 收到的 message。
1func TestSubscribedClientReceivesBroadcast(t *testing.T) {
2 hub := NewHub()
3 go hub.Run()
4
5 conn, _ := newTestWebSocket(t, newRouterWithHub(hub))
6
7 writeClientMessage(t, conn, ClientMessage{
8 Action: ActionSubscribeTopic,
9 Data: mustJSON(t, SubscribeTopicRequest{Topic: "alerts"}),
10 })
11 _ = readServerMessage(t, conn)
12
13 hub.Broadcast("alerts", ServerMessage{
14 Type: "notification",
15 Topic: "alerts",
16 })
17
18 pushed := readServerMessage(t, conn)
19 if pushed.Type != "notification" {
20 t.Fatalf("pushed type = %q, want notification", pushed.Type)
21 }
22}這個測試證明 subscribe state、hub broadcast、write pump 能透過真實 connection 協作。若只想測 Broadcast 是否檢查 topic,應寫 hub unit test,不必走 WebSocket。
【策略】非同步清理用 eventually,不用固定 sleep
連線清理測試的核心問題是 cleanup 通常非同步發生。測試應等待可觀察條件,而不是固定 sleep。
1func eventually(t *testing.T, timeout time.Duration, condition func() bool) {
2 t.Helper()
3
4 deadline := time.Now().Add(timeout)
5 for time.Now().Before(deadline) {
6 if condition() {
7 return
8 }
9 time.Sleep(10 * time.Millisecond)
10 }
11
12 t.Fatalf("condition was not met within %s", timeout)
13}使用方式:
1func TestClientIsRemovedAfterClose(t *testing.T) {
2 hub := NewHub()
3 conn, _ := newTestWebSocket(t, newRouterWithHub(hub))
4
5 eventually(t, time.Second, func() bool {
6 return hub.ClientCount() == 1
7 })
8
9 _ = conn.Close()
10
11 eventually(t, time.Second, func() bool {
12 return hub.ClientCount() == 0
13 })
14}eventually 不是任意等待;它等待具體條件。失敗時,測試會指出 cleanup 沒發生,而不是把時間耗掉後仍然不清楚原因。
【判讀】error action 應測協定,不只測 log
WebSocket action 失敗的核心語意是單次 action 失敗,不一定代表連線失敗。Integration test 應確認 server 回 error message,並且連線仍可繼續使用。
1func TestUnknownActionReturnsErrorMessage(t *testing.T) {
2 conn, _ := newTestWebSocket(t, newRouter())
3
4 writeClientMessage(t, conn, ClientMessage{
5 Action: "unknown_action",
6 })
7
8 response := readServerMessage(t, conn)
9 if response.Type != "error" {
10 t.Fatalf("response type = %q, want error", response.Type)
11 }
12}若設計上 unknown action 應直接關閉連線,也應明確測出 close 行為。重點是協定行為要可驗證,不要只依賴 server log。
本章不處理
本章先處理單一 Go server 內的 WebSocket 協定協作;跨節點 fan-out 與壓力測試,會在下列章節再往外延伸:
和 Go 教材的關係
這一章承接的是 WebSocket handler、pump 與 heartbeat;如果你要先回看語言教材,可以讀:
- Go:如何新增一個即時訊息 action
- Go:read/write pump 模式
- Go:heartbeat、deadline 與連線清理
- Go:graceful shutdown 與 signal handling
- Go 進階:CI、fuzz、load test 與 chaos testing
- Backend:可靠性驗證流程
小結
WebSocket integration test 應少量覆蓋關鍵端到端協定:dial、送 action、收 response、server push、錯誤回應與 cleanup。測試要使用真實 httptest.Server,每次 read 前設定 deadline,等待非同步清理時使用 eventually。單元測試負責大量規則,integration test 負責證明真實連線能把規則串起來。