本文目录导读:

Aiohttp 测试 WebSocket 非常方便,尤其适合异步场景。
Aiohttp 本身就支持 WebSocket 客户端和服务端的完整实现,测试时无需引入额外的库(如 websockets 或 socket.io),它的设计让异步 WebSocket 连接的创建、发送和接收都变得自然。
核心优势
- 原生支持:服务端和客户端都是
aiohttp的一部分,接口统一,学习成本低。 - 异步友好:完全基于
asyncio,测试代码不会阻塞事件循环。 - 可直接与
pytest配合:使用pytest-asyncio可以轻松编写异步测试用例。 - 内置测试客户端:
aiohttp.test_utils提供了AioHTTPTestCase和TestClient,可以启动一个真实的测试服务器,而不是 mock。
快速示例:测试 WebSocket 双向通信
假设你有一个简单的 WebSocket 服务端,它接收消息并原样返回(Echo):
# server.py (仅用于演示,实际测试中会内联)
from aiohttp import web
async def websocket_handler(request):
ws = web.WebSocketResponse()
await ws.prepare(request)
async for msg in ws:
if msg.type == web.WSMsgType.TEXT:
await ws.send_str(f"Echo: {msg.data}")
elif msg.type == web.WSMsgType.ERROR:
break
return ws
编写测试
import pytest
from aiohttp import web
from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop
# 方案一:基于类的测试 (传统方式)
class TestWebSocket(AioHTTPTestCase):
async def get_application(self):
app = web.Application()
app.router.add_get('/ws', self.websocket_handler)
return app
async def websocket_handler(self, request):
# 实际测试中,这里可以复用或内联生产代码
ws = web.WebSocketResponse()
await ws.prepare(request)
async for msg in ws:
if msg.type == web.WSMsgType.TEXT:
await ws.send_str(f"Echo: {msg.data}")
return ws
@unittest_run_loop
async def test_echo(self):
# 连接 WebSocket
resp = await self.client.ws_connect('/ws')
assert resp.status == 101 # 升级协议成功
# 发送消息
await resp.send_str("Hello")
# 接收响应
msg = await resp.receive()
assert msg.data == "Echo: Hello"
await resp.close()
更现代的 pytest 风格(推荐)
import pytest
from aiohttp import web
@pytest.fixture
def app():
app = web.Application()
app.router.add_get('/ws', websocket_handler)
return app
# 需要安装 pytest-aiohttp 插件
async def test_websocket_echo(aiohttp_client, app):
client = await aiohttp_client(app)
# 连接
ws = await client.ws_connect('/ws')
assert ws.closed is False
# 测试双向通信
await ws.send_str('ping')
response = await ws.receive()
assert response.data == 'Echo: ping'
# 测试关闭
await ws.close()
assert ws.closed
# 或者更直接地使用 aiohttp 客户端 (不需要服务器,适用于纯客户端测试)
async def test_websocket_connectivity():
async with aiohttp.ClientSession() as session:
async with session.ws_connect('ws://echo.websocket.org') as ws:
await ws.send_str('hello')
msg = await ws.receive(timeout=10)
assert msg.data == 'hello' # 公共 echo 服务器会原样返回
常见测试场景与代码片段
测试服务器主动推送 (如心跳)
async def test_server_push(aiohttp_client, app):
client = await aiohttp_client(app)
ws = await client.ws_connect('/ws')
# 假设服务器每 5 秒发送一次 "heartbeat"
msg = await ws.receive(timeout=10)
assert msg.data == "heartbeat"
测试不合法消息(如二进制、关闭帧)
async def test_binary_message_rejected(aiohttp_client, app):
client = await aiohttp_client(app)
ws = await client.ws_connect('/ws')
# 发送二进制数据 (假设服务端只接受文本)
await ws.send_bytes(b'\x00\x01')
# 服务端可能发送错误码或主动关闭
msg = await ws.receive()
assert msg.type == web.WSMsgType.CLOSE
assert msg.data == 1003 # 不支持的关闭码
测试并发连接
async def test_multiple_clients(aiohttp_client, app):
client = await aiohttp_client(app)
# 同时打开两个连接
ws1 = await client.ws_connect('/ws')
ws2 = await client.ws_connect('/ws')
await ws1.send_str("from ws1")
await ws2.send_str("from ws2")
assert (await ws1.receive()).data == "Echo: from ws1"
assert (await ws2.receive()).data == "Echo: from ws2"
可能遇到的问题及解决
| 问题 | 原因 | 解决 |
|---|---|---|
Connection closed 错误 |
测试结束时未正确关闭连接 | 使用 async with 或显式 await ws.close() |
| 接收超时 | 服务器未按预期发送消息 | 设置 receive(timeout=5) 参数 |
RuntimeError: Session is closed |
在 ClientSession 外使用 WebSocket |
确保操作在 async with session: 块内 |
无法连接到 localhost |
测试客户端未启动服务器 | 使用 aiohttp_client(app) 会自动创建测试服务器 |
pytest-asyncio 冲突 |
其他库使用不同的事件循环 | 明确设置 @pytest.mark.asyncio |
- 方便指数:★★★★★ (满分)
- 理由:Aiohttp 的 WebSocket 测试非常直观,代码量少,错误处理清晰,与
pytest-asyncio和aiohttp.test_utils结合时,可以模拟真实网络环境(包括并发、断线、异常消息)而无需外部工具。 - 建议:如果项目已经用 aiohttp 作为异步框架,完全不需要 引入其他 WebSocket 测试库,对于生产级测试,配合 pytest-timeout 和 pytest-rerunfailures 可以处理网络不稳定性。
如果你有具体的测试场景(比如需要测试鉴权、多房间广播、二进制协议),我可以提供更详细的代码示例。