第二十四章 集成测试:端到端信令流程验证

说实话,很多开发者写完信令服务器就觉得完事了。我之前也这么干过——单元测试全绿,就上线了。结果呢?两个客户端同时连接,信令乱成一锅粥。那次教训让我明白:单元测试只能保证零件没问题,集成测试才能证明机器能转起来

这一章,我们就来聊聊怎么给信令服务器做一次彻底的「体检」。我会带着你从手动测试开始,一步步搭建自动化测试脚本,最后实现多客户端并发模拟。嗯,都是我在真实项目中踩过的坑。

为什么集成测试这么重要?

你想想看,信令服务器本质上是个消息中转站。它要处理:

  • 多个客户端同时连接
  • 消息顺序和完整性
  • 连接断开后的状态清理
  • 房间内成员的同步

这些场景,单元测试根本覆盖不到。我见过一个项目,单元测试覆盖率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;

手动测试步骤

  1. 连接测试:启动两个客户端,确认都能收到 connected 事件
  2. 创建房间:客户端A发送 create-room,检查返回的房间ID
  3. 加入房间:客户端B用同样的房间ID发送 join-room
  4. 信令交换:模拟 SDP 和 ICE 候选的交换过程
  5. 断开测试:关闭一个客户端,检查另一个是否收到 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');
  });
});
注意: 测试用例之间要完全隔离。我吃过一次亏——前一个测试创建的房间没清理,后一个测试的房间ID冲突了。所以每个测试都要创建新的客户端实例。

多客户端并发模拟

单客户端测试只是开胃菜。真正的挑战在于:多个客户端同时操作时,服务器还能保持正确吗?

我曾经遇到一个场景: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-');
    });
  });
});

测试结果的可视化

跑完测试,光看控制台输出不够直观。我写了个小工具,把测试结果生成成流程图。

集成测试流程 1. 客户端连接 2. 创建房间 3. 加入房间 4. 信令交换(核心流程) Offer Answer ICE候选 连接建立 5. 验证与清理 消息完整性检查 连接状态验证 资源清理

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
经验之谈: CI 里跑集成测试,一定要加超时控制。我遇到过测试卡住,整个 CI 跑了40分钟才超时。现在我的每个测试用例都设了5秒超时,跑不通就快速失败。

常见问题与避坑

最后,分享几个我在集成测试中踩过的坑:

问题 表现 解决方案
端口冲突 测试启动失败 使用动态端口,或者测试前检查端口占用
消息乱序 收到错误的消息类型 给每条消息加序列号,接收端按序处理
资源泄漏 测试越跑越慢 每个测试后强制关闭所有 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);
  });
}

嗯,集成测试做到这个份上,信令服务器的质量基本就有保障了。记住:测试不是目的,质量才是。写测试花的时间,最终都会在线上故障排查中省回来。

公众号:蓝海资料掘金营,微信deep3321