WebSockets enable real-time bidirectional communication essential for chat apps, live feeds, and collaborative features. Testing WebSocket connections in mobile environments requires special attention to network reliability, battery consumption, and message ordering.

WebSocket Challenges in Mobile

Key Testing Areas

ChallengeImpactTesting Strategy
Connection DropsUser experience degradationReconnection logic testing
Message OrderingData consistency issuesSequence number validation
Battery DrainPoor user retentionConnection pooling tests
Network TransitionsWiFi ↔ cellular handoffSeamless reconnection tests
Background ModeConnection suspensionState restoration tests

Android WebSocket Testing

OkHttp WebSocket Implementation

import okhttp3.*
import okio.ByteString

class ChatWebSocketClient(private val serverUrl: String) {
    private var webSocket: WebSocket? = null
    private val client = OkHttpClient.Builder()
        .pingInterval(30, TimeUnit.SECONDS)
        .build()

    fun connect(listener: ChatListener) {
        val request = Request.Builder()
            .url(serverUrl)
            .build()

        webSocket = client.newWebSocket(request, object : WebSocketListener() {
            override fun onOpen(webSocket: WebSocket, response: Response) {
                listener.onConnected()
            }

            override fun onMessage(webSocket: WebSocket, text: String) {
                listener.onMessageReceived(text)
            }

            override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
                listener.onMessageReceived(bytes.utf8())
            }

            override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
                listener.onClosing(code, reason)
            }

            override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
                listener.onError(t)
                // Implement exponential backoff reconnection
                scheduleReconnect()
            }
        })
    }

    fun sendMessage(message: String) {
        webSocket?.send(message)
    }

    fun disconnect() {
        webSocket?.close(1000, "User disconnect")
    }

    private fun scheduleReconnect() {
        // Exponential backoff logic
        Handler(Looper.getMainLooper()).postDelayed({
            connect(currentListener)
        }, calculateBackoff())
    }
}

Testing Connection Stability

import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.test.runTest

class WebSocketConnectionTest {

    private lateinit var mockServer: MockWebServer
    private lateinit var chatClient: ChatWebSocketClient

    @Before
    fun setUp() {
        mockServer = MockWebServer()
        mockServer.start()

        chatClient = ChatWebSocketClient(mockServer.url("/").toString())
    }

    @After
    fun tearDown() {
        mockServer.shutdown()
    }

    @Test
    fun testSuccessfulConnection() = runTest {
        val listener = mockk<ChatListener>(relaxed = true)

        chatClient.connect(listener)

        delay(500)

        verify { listener.onConnected() }
    }

    @Test
    fun testConnectionFailureAndReconnect() = runTest {
        val listener = mockk<ChatListener>(relaxed = true)

        // First connection fails
        mockServer.shutdown()

        chatClient.connect(listener)

        delay(500)

        verify { listener.onError(any()) }

        // Restart server
        mockServer = MockWebServer()
        mockServer.start()

        // Wait for auto-reconnect
        delay(5000)

        verify(atLeast = 1) { listener.onConnected() }
    }

    @Test
    fun testMessageReceiving() = runTest {
        val listener = mockk<ChatListener>(relaxed = true)

        chatClient.connect(listener)

        // Simulate server sending message
        mockServer.enqueue(MockResponse()
            .withWebSocketUpgrade(object : WebSocketListener() {
                override fun onOpen(webSocket: WebSocket, response: Response) {
                    webSocket.send("""{"type":"message","text":"Hello"}""")
                }
            })
        )

        delay(1000)

        verify { listener.onMessageReceived(match { it.contains("Hello") }) }
    }

    @Test
    fun testMessageOrdering() = runTest {
        val receivedMessages = mutableListOf<String>()
        val listener = object : ChatListener {
            override fun onMessageReceived(message: String) {
                receivedMessages.add(message)
            }
        }

        chatClient.connect(listener)

        // Send multiple messages
        repeat(10) { index ->
            chatClient.sendMessage("Message $index")
            delay(10)
        }

        delay(1000)

        // Verify messages received in order
        receivedMessages.forEachIndexed { index, message ->
            assertTrue(message.contains("Message $index"))
        }
    }
}

Testing Network Transitions

import android.net.ConnectivityManager
import android.net.Network

class NetworkTransitionTest {

    @Test
    fun testWiFiToCellularTransition() = runTest {
        val listener = mockk<ChatListener>(relaxed = true)

        chatClient.connect(listener)

        // Simulate WiFi connection
        verify { listener.onConnected() }

        // Simulate WiFi disconnect
        simulateNetworkDisconnect()

        delay(100)

        // Simulate cellular connection
        simulateNetworkConnect(NetworkType.CELLULAR)

        delay(2000)

        // Verify reconnection happened
        verify(atLeast = 2) { listener.onConnected() }
    }

    @Test
    fun testBackgroundToForegroundTransition() = runTest {
        val listener = mockk<ChatListener>(relaxed = true)

        chatClient.connect(listener)

        // Move app to background
        chatClient.onAppBackground()

        delay(1000)

        // Verify connection suspended
        verify { listener.onConnectionSuspended() }

        // Move app to foreground
        chatClient.onAppForeground()

        delay(1000)

        // Verify connection restored
        verify { listener.onConnectionRestored() }
    }
}

iOS WebSocket Testing

URLSessionWebSocketTask Implementation

import Foundation

class ChatWebSocketClient {
    private var webSocketTask: URLSessionWebSocketTask?
    private let session: URLSession

    init(url: URL) {
        session = URLSession(configuration: .default)
        webSocketTask = session.webSocketTask(with: url)
    }

    func connect() {
        webSocketTask?.resume()
        receiveMessage()
    }

    func sendMessage(_ message: String) {
        let message = URLSessionWebSocketTask.Message.string(message)
        webSocketTask?.send(message) { error in
            if let error = error {
                print("WebSocket send error: \(error)")
            }
        }
    }

    private func receiveMessage() {
        webSocketTask?.receive { [weak self] result in
            switch result {
            case .success(let message):
                switch message {
                case .string(let text):
                    self?.handleMessage(text)
                case .data(let data):
                    self?.handleData(data)
                @unknown default:
                    break
                }
                // Continue receiving
                self?.receiveMessage()

            case .failure(let error):
                self?.handleError(error)
                self?.scheduleReconnect()
            }
        }
    }

    func disconnect() {
        webSocketTask?.cancel(with: .goingAway, reason: nil)
    }
}

Testing with XCTest

import XCTest
@testable import YourApp

class WebSocketTests: XCTestCase {
    var webSocketClient: ChatWebSocketClient!
    var mockServer: WebSocketMockServer!

    override func setUp() {
        super.setUp()
        mockServer = WebSocketMockServer()
        mockServer.start()

        webSocketClient = ChatWebSocketClient(url: mockServer.url)
    }

    override func tearDown() {
        webSocketClient.disconnect()
        mockServer.stop()
        super.tearDown()
    }

    func testConnectionEstablishment() {
        let expectation = expectation(description: "WebSocket connected")

        webSocketClient.onConnected = {
            expectation.fulfill()
        }

        webSocketClient.connect()

        waitForExpectations(timeout: 5.0)
    }

    func testMessageSendingAndReceiving() {
        let expectation = expectation(description: "Message received")
        var receivedMessage: String?

        webSocketClient.onMessage = { message in
            receivedMessage = message
            expectation.fulfill()
        }

        webSocketClient.connect()
        webSocketClient.sendMessage("Hello WebSocket")

        waitForExpectations(timeout: 5.0)

        XCTAssertEqual(receivedMessage, "Hello WebSocket")
    }

    func testAutoReconnect() {
        let connectionExpectation = expectation(description: "Reconnected")
        connectionExpectation.expectedFulfillmentCount = 2

        webSocketClient.onConnected = {
            connectionExpectation.fulfill()
        }

        webSocketClient.connect()

        // Wait for first connection
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            // Simulate server disconnect
            self.mockServer.closeAllConnections()
        }

        waitForExpectations(timeout: 10.0)
    }
}

Battery Optimization Testing

When testing WebSocket implementations, mobile app performance considerations are critical—particularly battery drain from persistent connections.

Connection Pooling

class OptimizedWebSocketManager {
    private val activeConnections = mutableMapOf<String, WebSocket>()

    fun getOrCreateConnection(channelId: String): WebSocket {
        return activeConnections.getOrPut(channelId) {
            createNewConnection(channelId)
        }
    }

    fun closeIdleConnections() {
        activeConnections.entries.removeIf { (_, socket) ->
            val isIdle = socket.idleTime() > IDLE_THRESHOLD
            if (isIdle) socket.close(1000, "Idle timeout")
            isIdle
        }
    }
}

@Test
fun testConnectionPooling() = runTest {
    val manager = OptimizedWebSocketManager()

    // Request same connection twice
    val conn1 = manager.getOrCreateConnection("channel-1")
    val conn2 = manager.getOrCreateConnection("channel-1")

    // Should reuse connection
    assertSame(conn1, conn2)

    // Different channel should create new connection
    val conn3 = manager.getOrCreateConnection("channel-2")
    assertNotSame(conn1, conn3)
}

Heartbeat Testing

class WebSocketHeartbeat(private val webSocket: WebSocket) {
    private val heartbeatJob = CoroutineScope(Dispatchers.IO).launch {
        while (isActive) {
            webSocket.send("ping")
            delay(30_000) // 30 seconds
        }
    }

    fun stop() {
        heartbeatJob.cancel()
    }
}

@Test
fun testHeartbeatPreventsDisconnect() = runTest {
    val heartbeat = WebSocketHeartbeat(webSocket)

    delay(90_000) // 90 seconds

    // Verify connection still alive
    assertTrue(webSocket.isConnected())

    heartbeat.stop()
}

Message Ordering and Reliability

Sequence Number Implementation

data class SequencedMessage(
    val sequenceNumber: Long,
    val payload: String,
    val timestamp: Long = System.currentTimeMillis()
)

class ReliableMessageQueue {
    private val pendingMessages = TreeMap<Long, SequencedMessage>()
    private var nextExpectedSequence = 0L

    fun processMessage(message: SequencedMessage): List<SequencedMessage> {
        val deliverable = mutableListOf<SequencedMessage>()

        pendingMessages[message.sequenceNumber] = message

        // Deliver messages in order
        while (pendingMessages.containsKey(nextExpectedSequence)) {
            deliverable.add(pendingMessages.remove(nextExpectedSequence)!!)
            nextExpectedSequence++
        }

        return deliverable
    }
}

@Test
fun testOutOfOrderMessages() {
    val queue = ReliableMessageQueue()

    // Receive messages out of order
    val msg2 = SequencedMessage(2, "Message 2")
    val msg0 = SequencedMessage(0, "Message 0")
    val msg1 = SequencedMessage(1, "Message 1")

    var delivered = queue.processMessage(msg2)
    assertEquals(0, delivered.size) // msg2 buffered

    delivered = queue.processMessage(msg0)
    assertEquals(1, delivered.size) // msg0 delivered

    delivered = queue.processMessage(msg1)
    assertEquals(2, delivered.size) // msg1 and buffered msg2 delivered

    assertEquals("Message 1", delivered[0].payload)
    assertEquals("Message 2", delivered[1].payload)
}

Performance and Load Testing

Similar to API performance testing, WebSocket endpoints require thorough load validation to ensure they handle concurrent connections and message throughput efficiently.

Stress Testing

@Test
fun testHighMessageThroughput() = runTest {
    val messagesCount = 10_000
    val receivedMessages = mutableListOf<String>()
    val startTime = System.currentTimeMillis()

    val listener = object : ChatListener {
        override fun onMessageReceived(message: String) {
            receivedMessages.add(message)
        }
    }

    chatClient.connect(listener)
    delay(500)

    // Send 10,000 messages
    repeat(messagesCount) { index ->
        chatClient.sendMessage("Message $index")
    }

    // Wait for all messages
    while (receivedMessages.size < messagesCount &&
           System.currentTimeMillis() - startTime < 60_000) {
        delay(100)
    }

    val duration = System.currentTimeMillis() - startTime
    val throughput = messagesCount / (duration / 1000.0)

    assertEquals(messagesCount, receivedMessages.size)
    assertTrue(throughput > 100, "Throughput: $throughput msg/s")
}

CI/CD Integration

Docker WebSocket Test Server

# docker-compose.yml
version: '3'

services:
  websocket-server:
    image: crossbario/autobahn-python:latest
    ports:
      - "9000:9000"
    volumes:
      - ./server.py:/app/server.py
    command: python /app/server.py

GitHub Actions

name: WebSocket Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      websocket:
        image: crossbario/autobahn-python:latest
        ports:
          - 9000:9000

    steps:
      - uses: actions/checkout@v3

      - name: Run WebSocket Tests
        run: ./gradlew test --tests '*WebSocketTest*'

Best Practices

  1. Always test reconnection logic - Network drops are common on mobile
  2. Implement message sequencing - Ensure data consistency
  3. Monitor battery impact - Use connection pooling and heartbeats wisely
  4. Test network transitions - WiFi ↔ cellular handoffs
  5. Handle background/foreground - Proper connection lifecycle management
  6. Use exponential backoff - Prevent server overload during reconnects

For comprehensive WebSocket API validation strategies, see our guide on API testing mastery, which covers authentication, error handling, and contract testing principles applicable to WebSocket endpoints.

Conclusion

WebSocket testing for mobile requires:

  • Connection stability testing with network simulation
  • Message ordering and reliability validation
  • Battery optimization verification
  • Background/foreground transition handling
  • Performance and load testing

Proper WebSocket testing ensures real-time features work reliably across varying network conditions while maintaining battery efficiency.