WebSocket作为现代Web应用中广泛使用的全双工通信协议,其测试工作往往比传统HTTP接口更加复杂。在实际项目中,我们经常遇到以下痛点:
针对这些问题,我设计了一套基于Python的高效WebSocket测试框架,核心优势体现在:
提示:框架完整代码已开源在GitHub,文末会提供获取方式。我们先从最核心的架构设计开始讲解。
经过多个项目的实践验证,最终确定的技术组合方案:
| 组件 | 选型 | 优势 | 适用场景 |
|---|---|---|---|
| 测试引擎 | pytest | 丰富的插件生态、简洁的fixture机制 | 所有测试场景 |
| WebSocket客户端 | websockets | 原生支持asyncio、API简洁 | Python 3.7+环境 |
| 报告生成 | pytest-html | 可视化报告、支持截图嵌入 | 需要展示测试结果的场景 |
| 分布式测试 | pytest-xdist | 多进程并行、自动负载均衡 | 大型测试套件执行 |
| 数据驱动 | pytest parametrize | 声明式数据注入、类型安全 | 多参数组合测试 |
选择websockets库而非更流行的aiohttp,主要基于以下考量:
框架采用经典的三层架构:
code复制├── core/ # 核心基础设施层
│ ├── __init__.py
│ ├── client.py # WebSocket客户端封装
│ └── runner.py # 测试执行器
├── cases/ # 测试用例层
│ ├── __init__.py
│ ├── test_connect.py # 连接测试
│ └── test_message.py # 消息测试
└── data/ # 测试数据层
├── __init__.py
├── schema/ # JSON Schema
└── testdata.json # 测试数据集
关键设计原则:
在core/client.py中实现增强版客户端:
python复制import asyncio
from websockets import connect, WebSocketClientProtocol
from typing import Optional, AsyncIterator
class WSClient:
def __init__(self, uri: str, timeout: int = 10):
self.uri = uri
self.timeout = timeout
self._conn: Optional[WebSocketClientProtocol] = None
async def __aenter__(self) -> 'WSClient':
self._conn = await connect(
self.uri,
ping_interval=None,
close_timeout=self.timeout
)
return self
async def __aexit__(self, *args) -> None:
if self._conn:
await self._conn.close()
async def send(self, message: str) -> None:
if not self._conn:
raise RuntimeError("Connection not established")
await self._conn.send(message)
async def recv(self) -> str:
if not self._conn:
raise RuntimeError("Connection not established")
return await self._conn.recv()
async def stream(self) -> AsyncIterator[str]:
if not self._conn:
raise RuntimeError("Connection not established")
async for message in self._conn:
yield message
关键特性:
在cases/test_message.py中展示标准测试用例:
python复制import pytest
from core.client import WSClient
@pytest.mark.asyncio
class TestMessageHandling:
@pytest.fixture
async def client(self):
async with WSClient("ws://localhost:8765") as c:
yield c
@pytest.mark.parametrize("msg,expected", [
("ping", "pong"),
("echo", "echo"),
("time", lambda r: isinstance(r, str))
])
async def test_basic_messages(self, client, msg, expected):
await client.send(msg)
response = await client.recv()
if callable(expected):
assert expected(response)
else:
assert response == expected
最佳实践:
在data/testdata.json中定义测试数据集:
json复制{
"auth_cases": [
{
"name": "valid_token",
"token": "eyJhbG...",
"should_pass": true,
"expected_code": 200
},
{
"name": "expired_token",
"token": "eyJhbG...",
"should_pass": false,
"expected_code": 401
}
]
}
通过自定义pytest插件实现智能加载:
python复制# conftest.py
import json
import pytest
from pathlib import Path
def pytest_generate_tests(metafunc):
if "auth_case" in metafunc.fixturenames:
test_data = json.loads(Path("data/testdata.json").read_text())
metafunc.parametrize("auth_case", test_data["auth_cases"])
配置pytest.ini实现动态并行:
ini复制[pytest]
addopts =
--html=report.html
-n auto
--dist=loadscope
--ws=4
关键参数说明:
-n auto:按CPU核心数自动设置进程数--dist=loadscope:相同测试类的用例分配到同一进程--ws=4:每个worker最多4个WebSocket连接典型错误现象:
code复制WebSocketTimeoutException: Connection timeout
排查步骤:
netstat -tulnp | grep 8765telnet localhost 8765python复制WSClient(uri, timeout=30) # 默认10秒改为30秒
解决方案:
python复制async def test_order_sensitive():
async with WSClient(uri) as client:
# 先发送所有消息
await client.send("first")
await client.send("second")
# 然后按顺序接收
assert await client.recv() == "first"
assert await client.recv() == "second"
关键点:
优化后的client实现:
python复制from typing import Dict
import weakref
class WSConnectionPool:
_instances: Dict[str, weakref.WeakSet] = {}
@classmethod
async def get_connection(cls, uri: str) -> WebSocketClientProtocol:
if uri not in cls._instances:
cls._instances[uri] = weakref.WeakSet()
for ws in list(cls._instances[uri]):
if not ws.closed:
return ws
ws = await connect(uri)
cls._instances[uri].add(ws)
return ws
优势:
配置压缩参数:
python复制async with connect(
uri,
compression="deflate", # 开启压缩
max_queue=1024 # 控制内存占用
) as ws:
...
性能对比数据:
| 消息大小 | 压缩前耗时 | 压缩后耗时 | 节省比例 |
|---|---|---|---|
| 1KB | 12ms | 15ms | -25% |
| 100KB | 105ms | 62ms | 41% |
| 1MB | 980ms | 420ms | 57% |
添加WSS安全连接:
python复制import ssl
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
async with connect(
"wss://example.com",
ssl=ssl_context
) as ws:
...
Jenkins Pipeline示例:
groovy复制pipeline {
agent any
stages {
stage('Test') {
steps {
sh 'python -m pytest -n 4 --html=report.html'
publishHTML target: [
allowMissing: false,
alwaysLinkToLastBuild: false,
keepAll: true,
reportDir: '.',
reportFiles: 'report.html',
reportName: 'WS Test Report'
]
}
}
}
}
在实际项目中,这套框架已经帮助我们:
框架的完整实现和示例代码可以通过GitHub仓库获取,地址见文末。对于特定项目的定制需求,建议从核心层开始逐步扩展,保持架构的简洁性和可维护性。