WebSocket Testing

Comprehensive guide to testing WebSocket channels and real-time features in Azu applications.

Overview

WebSocket testing in Azu focuses on testing real-time communication, channel connections, message handling, and live component interactions. This guide covers both unit and integration testing approaches for WebSocket functionality.

WebSocket Test Setup

Test Environment Configuration

# spec/websocket/spec_helper.cr
require "../spec_helper"

# WebSocket test configuration
CONFIG.websocket_test = {
  host: "localhost",
  port: 3001,
  timeout: 5.seconds,
  reconnect_attempts: 3
}

# WebSocket test utilities
module WebSocketTestHelpers
  def self.create_test_server
    server = HTTP::Server.new([
      Azu::Handler::Rescuer.new,
      Azu::Handler::Logger.new
    ])

    # Register test channels
    TestChannel.ws "/ws/test"
    ChatChannel.ws "/ws/chat"
    NotificationChannel.ws "/ws/notifications"

    server
  end

  def self.create_test_client(uri : String) : HTTP::WebSocket
    HTTP::WebSocket.new(uri)
  end
end

Test Channel Setup

# spec/websocket/test_channels.cr
class TestChannel < Azu::Channel
  ws "/ws/test"

  def on_connect
    # Test connection logic
    broadcast("user_connected", {user_id: @user_id})
  end

  def on_message(message)
    # Echo message back for testing
    broadcast("echo", {message: message})
  end

  def on_disconnect
    broadcast("user_disconnected", {user_id: @user_id})
  end
end

class ChatChannel < Azu::Channel
  ws "/ws/chat/:room_id"

  def on_connect
    subscribe_to_room(@request.params["room_id"])
  end

  def on_message(message)
    broadcast_to_room(@request.params["room_id"], "message", {
      user_id: @user_id,
      content: message,
      timestamp: Time.utc
    })
  end
end

Unit Testing WebSocket Channels

Channel Connection Testing

# spec/websocket/unit/channel_connection_spec.cr
require "../spec_helper"

describe TestChannel do
  describe "connection handling" do
    it "handles successful connections" do
      # Arrange
      channel = TestChannel.new
      mock_connection = MockWebSocketConnection.new

      # Act
      channel.on_connect(mock_connection)

      # Assert
      mock_connection.sent_messages.should contain("user_connected")
    end

    it "handles connection failures" do
      # Arrange
      channel = TestChannel.new
      mock_connection = MockWebSocketConnection.new(should_fail: true)

      # Act & Assert
      expect_raises(WebSocketConnectionError) do
        channel.on_connect(mock_connection)
      end
    end
  end
end

Message Handling Testing

# spec/websocket/unit/message_handling_spec.cr
describe TestChannel do
  describe "message handling" do
    it "processes valid messages" do
      # Arrange
      channel = TestChannel.new
      mock_connection = MockWebSocketConnection.new
      test_message = "Hello, WebSocket!"

      # Act
      channel.on_message(test_message, mock_connection)

      # Assert
      mock_connection.sent_messages.should contain("echo")
      mock_connection.last_message["message"].should eq(test_message)
    end

    it "handles malformed messages" do
      # Arrange
      channel = TestChannel.new
      mock_connection = MockWebSocketConnection.new
      malformed_message = ""

      # Act
      channel.on_message(malformed_message, mock_connection)

      # Assert
      mock_connection.sent_messages.should contain("error")
      mock_connection.last_message["error"].should eq("Message cannot be empty")
    end
  end
end

Channel State Testing

# spec/websocket/unit/channel_state_spec.cr
describe TestChannel do
  describe "channel state management" do
    it "tracks connected users" do
      # Arrange
      channel = TestChannel.new
      user1 = MockWebSocketConnection.new(user_id: "user1")
      user2 = MockWebSocketConnection.new(user_id: "user2")

      # Act
      channel.on_connect(user1)
      channel.on_connect(user2)

      # Assert
      channel.connected_users.size.should eq(2)
      channel.connected_users.should contain("user1")
      channel.connected_users.should contain("user2")
    end

    it "removes users on disconnect" do
      # Arrange
      channel = TestChannel.new
      user = MockWebSocketConnection.new(user_id: "user1")

      # Act
      channel.on_connect(user)
      channel.on_disconnect(user)

      # Assert
      channel.connected_users.size.should eq(0)
      channel.connected_users.should_not contain("user1")
    end
  end
end

Integration Testing WebSocket

Real WebSocket Connection Testing

# spec/websocket/integration/real_connection_spec.cr
require "../spec_helper"

describe "Real WebSocket Connections" do
  describe "end-to-end communication" do
    it "establishes connection and exchanges messages" do
      # Start test server
      server = WebSocketTestHelpers.create_test_server
      server.bind_tcp(CONFIG.websocket_test.host, CONFIG.websocket_test.port)

      spawn do
        server.listen
      end

      # Give server time to start
      sleep(0.1)

      # Create client connection
      client = WebSocketTestHelpers.create_test_client(
        "ws://#{CONFIG.websocket_test.host}:#{CONFIG.websocket_test.port}/ws/test"
      )

      received_messages = [] of String

      client.on_message do |message|
        received_messages << message
      end

      # Connect and send message
      client.connect
      client.send("Hello, Server!")

      # Wait for response
      sleep(0.5)

      # Assert
      received_messages.should contain("user_connected")
      received_messages.should contain("echo")

      # Cleanup
      client.close
      server.close
    end
  end
end

Multi-Client Testing

# spec/websocket/integration/multi_client_spec.cr
describe "Multi-Client WebSocket Testing" do
  it "broadcasts messages to all connected clients" do
    server = WebSocketTestHelpers.create_test_server
    server.bind_tcp(CONFIG.websocket_test.host, CONFIG.websocket_test.port)

    spawn { server.listen }
    sleep(0.1)

    # Create multiple clients
    clients = [] of HTTP::WebSocket
    received_messages = [] of Array(String)

    3.times do |i|
      client = WebSocketTestHelpers.create_test_client(
        "ws://#{CONFIG.websocket_test.host}:#{CONFIG.websocket_test.port}/ws/test"
      )

      messages = [] of String
      client.on_message { |msg| messages << msg }

      client.connect
      clients << client
      received_messages << messages
    end

    # Send message from first client
    clients[0].send("Broadcast message")

    # Wait for broadcast
    sleep(0.5)

    # Verify all clients received the message
    received_messages.each do |messages|
      messages.should contain("echo")
    end

    # Cleanup
    clients.each(&.close)
    server.close
  end
end

Live Component Testing

Component Rendering Testing

# spec/websocket/components/live_component_spec.cr
describe LiveUserListComponent do
  describe "real-time updates" do
    it "updates when users connect" do
      # Arrange
      component = LiveUserListComponent.new
      initial_html = component.render

      # Act - simulate user connection
      component.on_event("user_connected", {"user_id" => "user1", "name" => "John"})

      # Assert
      updated_html = component.render
      updated_html.should_not eq(initial_html)
      updated_html.should contain("John")
    end

    it "updates when users disconnect" do
      # Arrange
      component = LiveUserListComponent.new
      component.on_event("user_connected", {"user_id" => "user1", "name" => "John"})

      # Act - simulate user disconnection
      component.on_event("user_disconnected", {"user_id" => "user1"})

      # Assert
      html = component.render
      html.should_not contain("John")
    end
  end
end

Component Event Testing

# spec/websocket/components/component_events_spec.cr
describe ChatComponent do
  describe "message handling" do
    it "processes new messages" do
      # Arrange
      component = ChatComponent.new(room_id: "room1")

      # Act
      result = component.on_event("new_message", {
        "user_id" => "user1",
        "content" => "Hello, everyone!",
        "timestamp" => Time.utc.to_s
      })

      # Assert
      result.should be_a(Component::EventResult)
      result.action.should eq("append_message")

      # Verify message was added
      html = component.render
      html.should contain("Hello, everyone!")
    end

    it "handles message validation" do
      # Arrange
      component = ChatComponent.new(room_id: "room1")

      # Act - empty message
      result = component.on_event("new_message", {
        "user_id" => "user1",
        "content" => "",
        "timestamp" => Time.utc.to_s
      })

      # Assert
      result.action.should eq("show_error")
      result.data["error"].should eq("Message cannot be empty")
    end
  end
end

Spark System Testing

Spark Event Testing

# spec/websocket/spark/spark_events_spec.cr
describe Azu::Spark do
  describe "event broadcasting" do
    it "broadcasts events to connected clients" do
      # Arrange
      spark = Azu::Spark.new
      mock_client = MockSparkClient.new
      spark.add_client(mock_client)

      # Act
      spark.broadcast("user_updated", {"user_id" => "user1", "name" => "Updated Name"})

      # Assert
      mock_client.received_events.should contain("user_updated")
      mock_client.last_event_data["user_id"].should eq("user1")
    end

    it "handles client disconnection" do
      # Arrange
      spark = Azu::Spark.new
      mock_client = MockSparkClient.new
      spark.add_client(mock_client)

      # Act
      spark.remove_client(mock_client)
      spark.broadcast("test_event", {"data" => "test"})

      # Assert
      mock_client.received_events.should_not contain("test_event")
    end
  end
end

Spark Channel Testing

# spec/websocket/spark/spark_channels_spec.cr
describe Azu::Spark::Channel do
  describe "channel subscriptions" do
    it "subscribes clients to channels" do
      # Arrange
      channel = Azu::Spark::Channel.new("test_channel")
      mock_client = MockSparkClient.new

      # Act
      channel.subscribe(mock_client)
      channel.broadcast("channel_message", {"content" => "Hello"})

      # Assert
      mock_client.received_events.should contain("channel_message")
    end

    it "unsubscribes clients from channels" do
      # Arrange
      channel = Azu::Spark::Channel.new("test_channel")
      mock_client = MockSparkClient.new
      channel.subscribe(mock_client)

      # Act
      channel.unsubscribe(mock_client)
      channel.broadcast("channel_message", {"content" => "Hello"})

      # Assert
      mock_client.received_events.should_not contain("channel_message")
    end
  end
end

Mock Objects for Testing

Mock WebSocket Connection

# spec/websocket/mocks/mock_websocket.cr
class MockWebSocketConnection
  property user_id : String
  property sent_messages : Array(String)
  property should_fail : Bool

  def initialize(@user_id = "test_user", @should_fail = false)
    @sent_messages = [] of String
  end

  def send(message : String)
    raise WebSocketConnectionError.new if @should_fail
    @sent_messages << message
  end

  def close
    # Mock close behavior
  end

  def last_message : Hash(String, String)
    return {} of String => String if @sent_messages.empty?

    # Parse last message as JSON
    JSON.parse(@sent_messages.last).as_h.transform_values(&.to_s)
  end
end

Mock Spark Client

# spec/websocket/mocks/mock_spark_client.cr
class MockSparkClient
  property received_events : Array(String)
  property last_event_data : Hash(String, String)

  def initialize
    @received_events = [] of String
    @last_event_data = {} of String => String
  end

  def send_event(event_name : String, data : Hash)
    @received_events << event_name
    @last_event_data = data.transform_values(&.to_s)
  end

  def subscribed_channels : Array(String)
    # Mock subscribed channels
    [] of String
  end
end

Performance Testing

Connection Load Testing

# spec/websocket/performance/load_spec.cr
describe "WebSocket Load Testing" do
  it "handles multiple concurrent connections" do
    server = WebSocketTestHelpers.create_test_server
    server.bind_tcp(CONFIG.websocket_test.host, CONFIG.websocket_test.port)

    spawn { server.listen }
    sleep(0.1)

    # Create many concurrent connections
    clients = [] of HTTP::WebSocket
    connection_count = 100

    connection_count.times do |i|
      client = WebSocketTestHelpers.create_test_client(
        "ws://#{CONFIG.websocket_test.host}:#{CONFIG.websocket_test.port}/ws/test"
      )

      begin
        client.connect
        clients << client
      rescue ex
        # Handle connection failures
      end
    end

    # Verify most connections succeeded
    successful_connections = clients.size
    success_rate = successful_connections.to_f / connection_count

    success_rate.should be >= 0.95 # 95% success rate

    # Cleanup
    clients.each(&.close)
    server.close
  end
end

Message Throughput Testing

# spec/websocket/performance/throughput_spec.cr
describe "WebSocket Message Throughput" do
  it "handles high message volume" do
    server = WebSocketTestHelpers.create_test_server
    server.bind_tcp(CONFIG.websocket_test.host, CONFIG.websocket_test.port)

    spawn { server.listen }
    sleep(0.1)

    client = WebSocketTestHelpers.create_test_client(
      "ws://#{CONFIG.websocket_test.host}:#{CONFIG.websocket_test.port}/ws/test"
    )

    received_count = 0
    client.on_message { |msg| received_count += 1 }
    client.connect

    # Send many messages quickly
    start_time = Time.monotonic
    1000.times do |i|
      client.send("Message #{i}")
    end

    # Wait for processing
    sleep(2)
    end_time = Time.monotonic

    # Calculate throughput
    duration = end_time - start_time
    throughput = received_count / duration.total_seconds

    # Assert reasonable throughput
    throughput.should be >= 100 # At least 100 messages per second

    client.close
    server.close
  end
end

Error Handling Testing

Connection Error Testing

# spec/websocket/errors/connection_errors_spec.cr
describe "WebSocket Error Handling" do
  it "handles connection timeouts" do
    # Test with slow server
    slow_server = HTTP::Server.new do |context|
      sleep(10) # Simulate slow response
    end

    slow_server.bind_tcp(CONFIG.websocket_test.host, CONFIG.websocket_test.port)
    spawn { slow_server.listen }

    client = WebSocketTestHelpers.create_test_client(
      "ws://#{CONFIG.websocket_test.host}:#{CONFIG.websocket_test.port}/ws/test"
    )

    # Should timeout
    expect_raises(HTTP::TimeoutError) do
      client.connect(timeout: 1.second)
    end

    slow_server.close
  end

  it "handles malformed messages" do
    server = WebSocketTestHelpers.create_test_server
    server.bind_tcp(CONFIG.websocket_test.host, CONFIG.websocket_test.port)

    spawn { server.listen }
    sleep(0.1)

    client = WebSocketTestHelpers.create_test_client(
      "ws://#{CONFIG.websocket_test.host}:#{CONFIG.websocket_test.port}/ws/test"
    )

    client.connect

    # Send malformed message
    client.send("") # Empty message

    # Should handle gracefully
    sleep(0.1)

    client.close
    server.close
  end
end

Test Utilities

WebSocket Test Helpers

# spec/websocket/helpers/websocket_helpers.cr
module WebSocketHelpers
  def self.wait_for_message(client : HTTP::WebSocket, expected_message : String, timeout : Time::Span = 5.seconds) : Bool
    start_time = Time.monotonic
    received_messages = [] of String

    client.on_message do |message|
      received_messages << message
      return true if message.includes?(expected_message)
    end

    while Time.monotonic - start_time < timeout
      sleep(0.1)
      return true if received_messages.any? { |msg| msg.includes?(expected_message) }
    end

    false
  end

  def self.create_test_message(type : String, data : Hash) : String
    {
      "type" => type,
      "data" => data,
      "timestamp" => Time.utc.to_s
    }.to_json
  end
end

Running WebSocket Tests

Test Commands

# Run all WebSocket tests
crystal spec spec/websocket/

# Run unit tests only
crystal spec spec/websocket/unit/

# Run integration tests only
crystal spec spec/websocket/integration/

# Run with verbose output
crystal spec spec/websocket/ --verbose

# Run specific test file
crystal spec spec/websocket/unit/channel_connection_spec.cr

CI/CD Integration

# .github/workflows/websocket-tests.yml
name: WebSocket Tests

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  websocket-tests:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
      - name: Setup Crystal
        uses: crystal-lang/install-crystal@v1
      - name: Install dependencies
        run: shards install
      - name: Run WebSocket tests
        run: crystal spec spec/websocket/
      - name: Run WebSocket performance tests
        run: crystal spec spec/websocket/performance/

Best Practices

1. Test Realistic Scenarios

# Test realistic WebSocket usage patterns
describe "Realistic WebSocket Usage" do
  it "handles typical chat room scenario" do
    # Setup chat room
    # Connect multiple users
    # Exchange messages
    # Handle disconnections
    # Verify message delivery
  end
end

2. Test Edge Cases

# Test edge cases
describe "WebSocket Edge Cases" do
  it "handles rapid connect/disconnect cycles" do
    # Test rapid connection attempts
  end

  it "handles large message payloads" do
    # Test messages exceeding size limits
  end

  it "handles network interruptions" do
    # Test connection drops and reconnections
  end
end

3. Test Performance Boundaries

# Test performance boundaries
describe "WebSocket Performance Boundaries" do
  it "handles maximum concurrent connections" do
    # Test system limits
  end

  it "handles maximum message frequency" do
    # Test rate limiting
  end
end

Next Steps


WebSocket testing ensures your real-time features work reliably and handle edge cases gracefully.

Last updated

Was this helpful?