第二十四章 集成测试:端到端信令流程验证
说实话,很多开发者写完信令服务器就觉得完事了。我之前也这么干过——单元测试全绿,就上线了。结果呢?两个客户端同时连接,信令乱成一锅粥。那次教训让我明白:单元测试只能保证零件没问题,集成测试才能证明机器能转起来。
这一章,我们就来聊聊怎么给信令服务器做一次彻底的「体检」。我会带着你从手动测试开始,一步步搭建自动化测试脚本,最后实现多客户端并发模拟。嗯,都是我在真实项目中踩过的坑。
为什么集成测试这么重要?
你想想看,信令服务器本质上是个消息中转站。它要处理:
- 多个客户端同时连接
- 消息顺序和完整性
- 连接断开后的状态清理
- 房间内成员的同步
这些场景,单元测试根本覆盖不到。我见过一个项目,单元测试覆盖率90%以上,结果上线第一天就崩了——因为两个用户同时加入房间时,服务器把房间号搞混了。
手动测试:先跑通基本流程
在写自动化脚本之前,我建议你先手动跑一遍核心流程。不是为了验证功能,而是为了理解「正常情况」应该是什么样。
测试环境准备
我们需要三个终端:
- 终端1:运行信令服务器
- 终端2:客户端A(发起方)
- 终端3:客户端B(接收方)
我用的是 Node.js 的 WebSocket 客户端来模拟。你也可以用 wscat 之类的工具,但我个人习惯写脚本,方便后面复用。
// 客户端模拟脚本 client-simulator.js
const WebSocket = require('ws');
class ClientSimulator {
constructor(name, serverUrl) {
this.name = name;
this.ws = new WebSocket(serverUrl);
this.messages = [];
this.ws.on('message', (data) => {
const msg = JSON.parse(data);
this.messages.push(msg);
console.log(`[${this.name}] 收到:`, msg.type);
});
}
send(type, payload) {
this.ws.send(JSON.stringify({ type, payload }));
}
waitForMessage(expectedType, timeout = 5000) {
return new Promise((resolve, reject) => {
const check = () => {
const found = this.messages.find(m => m.type === expectedType);
if (found) resolve(found);
else setTimeout(check, 100);
};
setTimeout(() => reject(new Error('超时')), timeout);
check();
});
}
}
module.exports = ClientSimulator;
手动测试步骤
- 连接测试:启动两个客户端,确认都能收到
connected事件 - 创建房间:客户端A发送
create-room,检查返回的房间ID - 加入房间:客户端B用同样的房间ID发送
join-room - 信令交换:模拟 SDP 和 ICE 候选的交换过程
- 断开测试:关闭一个客户端,检查另一个是否收到
peer-disconnected
自动化测试脚本:让机器替我们干活
手动测试跑通后,就该写自动化脚本了。我用的测试框架是 Mocha + Chai,配合上面写的 ClientSimulator 类。
基础测试用例
// test/integration/signaling.test.js
const { expect } = require('chai');
const ClientSimulator = require('./client-simulator');
const SERVER_URL = 'ws://localhost:8080';
describe('信令服务器集成测试', () => {
let clientA, clientB;
beforeEach(async () => {
clientA = new ClientSimulator('A', SERVER_URL);
clientB = new ClientSimulator('B', SERVER_URL);
// 等待两个客户端都连接成功
await Promise.all([
clientA.waitForMessage('connected'),
clientB.waitForMessage('connected')
]);
});
afterEach(() => {
clientA.ws.close();
clientB.ws.close();
});
it('应该能成功创建房间', async () => {
clientA.send('create-room', {});
const response = await clientA.waitForMessage('room-created');
expect(response.payload).to.have.property('roomId');
expect(response.payload.roomId).to.be.a('string');
});
it('应该能完成端到端信令交换', async () => {
// 创建房间
clientA.send('create-room', {});
const roomCreated = await clientA.waitForMessage('room-created');
const roomId = roomCreated.payload.roomId;
// 客户端B加入
clientB.send('join-room', { roomId });
await clientB.waitForMessage('room-joined');
// 模拟SDP交换
clientA.send('offer', {
roomId,
sdp: 'fake-sdp-from-A'
});
const offerReceived = await clientB.waitForMessage('offer');
expect(offerReceived.payload.sdp).to.equal('fake-sdp-from-A');
// 模拟ICE候选
clientB.send('ice-candidate', {
roomId,
candidate: 'candidate-from-B'
});
const iceReceived = await clientA.waitForMessage('ice-candidate');
expect(iceReceived.payload.candidate).to.equal('candidate-from-B');
});
it('应该处理客户端断开连接', async () => {
// 先建立房间
clientA.send('create-room', {});
const { payload } = await clientA.waitForMessage('room-created');
clientB.send('join-room', { roomId: payload.roomId });
await clientB.waitForMessage('room-joined');
// 断开客户端A
clientA.ws.close();
// 客户端B应该收到断开通知
const disconnectMsg = await clientB.waitForMessage('peer-disconnected');
expect(disconnectMsg.payload.peerId).to.equal('A');
});
});
多客户端并发模拟
单客户端测试只是开胃菜。真正的挑战在于:多个客户端同时操作时,服务器还能保持正确吗?
我曾经遇到一个场景:10个客户端同时加入同一个房间,结果服务器只通知了其中8个人。排查了半天,发现是房间成员列表的并发写入问题。
并发测试脚本
// test/integration/concurrent.test.js
describe('并发场景测试', () => {
const CONCURRENT_CLIENTS = 20;
it('应该能处理多个客户端同时加入', async () => {
// 创建房间
const host = new ClientSimulator('host', SERVER_URL);
await host.waitForMessage('connected');
host.send('create-room', {});
const { payload } = await host.waitForMessage('room-created');
const roomId = payload.roomId;
// 并发加入
const joinPromises = [];
for (let i = 0; i < CONCURRENT_CLIENTS; i++) {
const client = new ClientSimulator(`client-${i}`, SERVER_URL);
joinPromises.push(
client.waitForMessage('connected').then(() => {
client.send('join-room', { roomId });
return client.waitForMessage('room-joined');
})
);
}
const results = await Promise.all(joinPromises);
expect(results).to.have.lengthOf(CONCURRENT_CLIENTS);
// 验证所有客户端都收到了正确的房间信息
results.forEach((msg, index) => {
expect(msg.payload.roomId).to.equal(roomId);
expect(msg.payload.members).to.include(`client-${index}`);
});
});
it('应该能处理并发信令交换', async () => {
// 创建房间并加入多个客户端
// ... 省略重复代码
// 同时发送信令
const signalPromises = clients.map((client, index) => {
client.send('offer', {
roomId,
sdp: `sdp-from-${index}`
});
return client.waitForMessage('offer');
});
const signals = await Promise.all(signalPromises);
// 验证每个客户端都收到了其他所有人的信令
signals.forEach((msg, index) => {
// 这里要检查消息是否完整
expect(msg.payload.sdp).to.include('sdp-from-');
});
});
});
测试结果的可视化
跑完测试,光看控制台输出不够直观。我写了个小工具,把测试结果生成成流程图。
CI/CD 集成
自动化测试写好了,下一步就是让它自动跑。我一般会在 CI 流程里加一个步骤:每次提交代码,自动启动信令服务器,跑一遍集成测试。
# .github/workflows/test.yml
name: 信令服务器集成测试
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 启动信令服务器
run: |
npm install
node server.js &
sleep 2 # 等待服务器启动
- name: 运行集成测试
run: npm run test:integration
- name: 生成测试报告
run: npm run test:report
- name: 清理
run: kill $(lsof -t -i:8080) || true
常见问题与避坑
最后,分享几个我在集成测试中踩过的坑:
| 问题 | 表现 | 解决方案 |
|---|---|---|
| 端口冲突 | 测试启动失败 | 使用动态端口,或者测试前检查端口占用 |
| 消息乱序 | 收到错误的消息类型 | 给每条消息加序列号,接收端按序处理 |
| 资源泄漏 | 测试越跑越慢 | 每个测试后强制关闭所有 WebSocket 连接 |
| 状态残留 | 测试结果不稳定 | 每次测试前重启服务器,或者清理所有房间 |
我曾经因为没处理好端口冲突,浪费了整整一个下午。后来学乖了,写了个端口检测函数,测试启动前先确认端口可用。
function checkPort(port) {
return new Promise((resolve, reject) => {
const server = require('net').createServer();
server.once('error', (err) => {
if (err.code === 'EADDRINUSE') reject(new Error(`端口 ${port} 已被占用`));
});
server.once('listening', () => {
server.close();
resolve();
});
server.listen(port);
});
}
嗯,集成测试做到这个份上,信令服务器的质量基本就有保障了。记住:测试不是目的,质量才是。写测试花的时间,最终都会在线上故障排查中省回来。