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
Challenge | Impact | Testing Strategy |
---|---|---|
Connection Drops | User experience degradation | Reconnection logic testing |
Message Ordering | Data consistency issues | Sequence number validation |
Battery Drain | Poor user retention | Connection pooling tests |
Network Transitions | WiFi ↔ cellular handoff | Seamless reconnection tests |
Background Mode | Connection suspension | State 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
- Always test reconnection logic - Network drops are common on mobile
- Implement message sequencing - Ensure data consistency
- Monitor battery impact - Use connection pooling and heartbeats wisely
- Test network transitions - WiFi ↔ cellular handoffs
- Handle background/foreground - Proper connection lifecycle management
- 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.