Testing WebSockets
Test WebSocket handlers using TestClient.websocketConnect(). The test client creates an in-memory duplex connection that exercises the full WebSocket handler without a real network socket.
Overview
WebSocket handlers need to be tested for correct message handling, echo behavior, error handling, and connection lifecycle. Zigmund's TestClient provides websocketConnect(), which returns an in-memory WebSocket connection that can send and receive messages synchronously -- no network stack required.
Example
const std = @import("std");
const zigmund = @import("zigmund");
/// Demonstrates WebSocket testing using TestClient.websocketConnect().
/// The test client creates an in-memory duplex connection that exercises
/// the full WebSocket handler without a real network socket.
fn echoHandler(conn: *zigmund.runtime.websocket.Connection, _: *zigmund.Request, _: std.mem.Allocator) anyerror!void {
while (true) {
const msg = conn.receiveSmall() catch |err| switch (err) {
error.ConnectionClosed => return,
else => return err,
};
switch (msg.opcode) {
.text => try conn.sendText(msg.data),
.binary => try conn.sendBinary(msg.data),
else => {},
}
}
}
fn wsTestInfo(_: *zigmund.Request, allocator: std.mem.Allocator) !zigmund.Response {
return zigmund.Response.json(allocator, .{
.message = "WebSocket echo server for testing — connect at /ws",
});
}
/// Example test usage:
///
/// var client = zigmund.TestClient.init(std.testing.allocator, &app);
/// defer client.deinit();
///
/// var ws = try client.websocketConnect("/ws");
/// defer ws.deinit();
///
/// try ws.sendText("hello");
/// const reply = try ws.receiveSmall();
/// // reply.data == "hello", reply.opcode == .text
pub fn buildExample(app: *zigmund.App) !void {
try app.get("/ws/info", wsTestInfo, .{
.summary = "WebSocket testing info endpoint",
});
try app.websocket("/ws", echoHandler, .{
.summary = "WebSocket echo handler for TestClient testing",
});
}
How It Works
1. Setting Up a WebSocket Test
Create a TestClient and connect to a WebSocket endpoint:
var client = zigmund.TestClient.init(std.testing.allocator, &app);
defer client.deinit();
var ws = try client.websocketConnect("/ws");
defer ws.deinit();
The websocketConnect() method returns a test WebSocket connection that bypasses the network layer.
2. Sending Messages
Use the same API as production WebSocket connections:
try ws.sendText("hello"); // Send a text frame
try ws.sendBinary(&data_bytes); // Send a binary frame
3. Receiving Messages
Receive messages synchronously:
const reply = try ws.receiveSmall();
The returned message has:
- reply.opcode -- the frame type (.text, .binary, etc.).
- reply.data -- the payload as a byte slice.
4. Full Test Pattern
test "echo WebSocket handler" {
var app = try zigmund.App.init(std.testing.allocator, .{
.title = "Test", .version = "1.0",
});
defer app.deinit();
try buildExample(&app);
var client = zigmund.TestClient.init(std.testing.allocator, &app);
defer client.deinit();
var ws = try client.websocketConnect("/ws");
defer ws.deinit();
// Test text echo
try ws.sendText("hello");
const text_reply = try ws.receiveSmall();
try std.testing.expectEqualStrings("hello", text_reply.data);
try std.testing.expectEqual(.text, text_reply.opcode);
// Test binary echo
const binary_data = &[_]u8{ 0x01, 0x02, 0x03 };
try ws.sendBinary(binary_data);
const binary_reply = try ws.receiveSmall();
try std.testing.expectEqualSlices(u8, binary_data, binary_reply.data);
try std.testing.expectEqual(.binary, binary_reply.opcode);
}
5. Testing Connection Lifecycle
Test that the handler properly handles disconnections:
var ws = try client.websocketConnect("/ws");
// ws.deinit() closes the connection, which should cause
// the handler to receive error.ConnectionClosed and return cleanly
ws.deinit();
6. Testing Multiple Messages
Send and receive multiple messages in sequence to verify stateful handler behavior:
try ws.sendText("first");
const r1 = try ws.receiveSmall();
try ws.sendText("second");
const r2 = try ws.receiveSmall();
try ws.sendText("third");
const r3 = try ws.receiveSmall();
Key Points
TestClient.websocketConnect()creates an in-memory WebSocket connection for testing.- The test connection uses the same API as production connections (
sendText,receiveSmall, etc.). - No network stack is involved -- tests run entirely in-process.
- Always use
defer ws.deinit()to properly close the connection and clean up resources. - Test both text and binary message types to ensure full handler coverage.
- Test the disconnection path by closing the connection and verifying graceful handler exit.
See Also
- WebSockets -- production WebSocket handler patterns.
- Testing Events -- test lifecycle hooks.
- Async Tests -- integration testing patterns.