29、WebRTC测试:单元测试(Jest)、端到端测试(Puppeteer)、媒体质量测试(客观指标)、压力测试

说实话,很多做WebRTC的同学,写完代码能跑通就谢天谢地了。但我在实际项目中吃过不少亏——有一次上线前没做压力测试,结果用户一多,服务器直接崩了。从那以后,我养成了一个习惯:测试不是可选项,而是必须项。

今天咱们就聊聊WebRTC的四大测试维度。我会把我在项目中踩过的坑、总结的经验,一股脑儿倒出来。

一、单元测试:用Jest守住代码底线

单元测试,说白了就是测你的函数对不对。WebRTC里有很多逻辑密集的模块,比如SDP解析、码率计算、ICE状态机。这些地方一旦出错,排查起来特别痛苦。

我个人习惯用Jest,轻量、速度快,而且对TypeScript支持很好。

1.1 测试SDP解析函数

举个例子,我们经常需要从SDP里提取视频的编码参数:

// sdpParser.js
function extractVideoCodec(sdp) {
  const match = sdp.match(/a=rtpmap:(\d+) H264/);
  return match ? match[1] : null;
}

// sdpParser.test.js
test('should extract H264 payload type from SDP', () => {
  const sdp = `a=rtpmap:96 H264/90000`;
  expect(extractVideoCodec(sdp)).toBe('96');
});

test('should return null if no H264 found', () => {
  const sdp = `a=rtpmap:97 VP8/90000`;
  expect(extractVideoCodec(sdp)).toBeNull();
});

嗯,这里要注意:SDP的格式在不同浏览器里可能有细微差别。我曾经遇到过Chrome和Firefox生成的SDP里,rtpmap行的顺序不一样,导致解析失败。所以测试用例里最好覆盖多种浏览器生成的SDP样本。

1.2 测试ICE状态机

ICE状态机是WebRTC里最容易出bug的地方之一。我建议把状态迁移逻辑单独抽出来测试:

// iceStateMachine.js
const STATES = {
  NEW: 'new',
  CHECKING: 'checking',
  CONNECTED: 'connected',
  FAILED: 'failed'
};

function transition(currentState, event) {
  switch (currentState) {
    case STATES.NEW:
      if (event === 'start') return STATES.CHECKING;
      break;
    case STATES.CHECKING:
      if (event === 'success') return STATES.CONNECTED;
      if (event === 'timeout') return STATES.FAILED;
      break;
    default:
      return currentState;
  }
  return currentState;
}

test('should transition from NEW to CHECKING on start', () => {
  expect(transition('new', 'start')).toBe('checking');
});

test('should stay in CONNECTED on any event', () => {
  expect(transition('connected', 'timeout')).toBe('connected');
});

你看,这样写测试,每个状态迁移都是可验证的。我遇到过最坑的一次,是ICE状态从CHECKING直接跳到了FAILED,但日志里没有任何错误信息。后来发现是底层网络库抛了个异常,但没被捕获。单元测试帮我提前发现了这个漏洞。

我的小技巧: 用Jest的describe和beforeEach来组织测试,每个测试文件只测一个模块。这样跑测试时,哪个模块挂了一目了然。

二、端到端测试:用Puppeteer模拟真实用户

单元测试只能保证函数逻辑正确,但两个浏览器之间能不能真正建立连接、音视频能不能正常传输,这得靠端到端测试。

我推荐用Puppeteer,它可以控制无头Chrome,模拟两个用户加入同一个房间。

2.1 搭建测试环境

首先,你需要一个信令服务器。我一般用Node.js写一个简单的WebSocket服务器:

// signalingServer.js
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
  ws.on('message', (message) => {
    // 广播给其他客户端
    wss.clients.forEach(client => {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(message);
      }
    });
  });
});

2.2 编写Puppeteer测试脚本

// e2e.test.js
const puppeteer = require('puppeteer');

test('two peers should establish a WebRTC connection', async () => {
  const browser = await puppeteer.launch({ headless: true });
  const page1 = await browser.newPage();
  const page2 = await browser.newPage();

  await page1.goto('http://localhost:3000');
  await page2.goto('http://localhost:3000');

  // 模拟点击"加入房间"按钮
  await page1.click('#join-btn');
  await page2.click('#join-btn');

  // 等待连接建立
  await page1.waitForSelector('.connection-state.connected', { timeout: 10000 });
  await page2.waitForSelector('.connection-state.connected', { timeout: 10000 });

  // 验证双方都收到了对方的媒体流
  const remoteVideo1 = await page1.$eval('#remote-video', el => el.srcObject);
  const remoteVideo2 = await page2.$eval('#remote-video', el => el.srcObject);

  expect(remoteVideo1).not.toBeNull();
  expect(remoteVideo2).not.toBeNull();

  await browser.close();
});

这里有个坑:Puppeteer默认的无头模式可能不支持WebRTC的某些特性。我建议用headless: 'new'模式,或者直接用有头模式调试。另外,记得在页面里暴露一些状态信息,比如connectionStateiceConnectionState,方便测试脚本读取。

注意: 端到端测试比较慢,建议只在CI/CD的预发布环境跑,不要每次提交都跑。我一般把E2E测试放在合并到主分支之前的流水线里。

三、媒体质量测试:用客观指标说话

主观感受很重要,但你不能靠"我觉得画面挺清晰"来交付产品。我们需要客观指标:PSNR、SSIM、VMAF、延迟、丢包率。

我个人习惯用getStats() API来采集实时数据,然后计算这些指标。

3.1 采集WebRTC统计信息

// 每隔1秒采集一次统计信息
setInterval(async () => {
  const stats = await peerConnection.getStats();
  stats.forEach(report => {
    if (report.type === 'inbound-rtp' && report.kind === 'video') {
      console.log('丢包率:', report.packetsLost / report.packetsReceived);
      console.log('抖动:', report.jitter);
      console.log('帧率:', report.framesPerSecond);
    }
  });
}, 1000);

3.2 计算PSNR

PSNR是衡量视频质量的经典指标。虽然WebRTC不直接提供,但你可以通过比较原始帧和接收帧来计算:

function calculatePSNR(original, received) {
  let mse = 0;
  for (let i = 0; i < original.length; i++) {
    mse += (original[i] - received[i]) ** 2;
  }
  mse /= original.length;
  if (mse === 0) return Infinity;
  return 10 * Math.log10(255 ** 2 / mse);
}

嗯,这里要注意:PSNR对像素级差异敏感,但人眼其实对某些失真不敏感。所以现在业界更推荐VMAF(Video Multi-Method Assessment Fusion),它结合了多种指标,更接近人眼感受。

3.3 延迟测试

延迟是实时通信的生命线。我一般用两种方法测:

  • RTT测量: 通过RTCP的Sender Report和Receiver Report计算往返时间
  • 端到端延迟: 在发送端打时间戳,接收端解码后显示时间差
// 发送端
const startTime = Date.now();
const dataChannel = peerConnection.createDataChannel('latency');
dataChannel.send(JSON.stringify({ type: 'ping', time: startTime }));

// 接收端
dataChannel.onmessage = (event) => {
  const { type, time } = JSON.parse(event.data);
  if (type === 'ping') {
    const latency = Date.now() - time;
    console.log('端到端延迟:', latency, 'ms');
  }
};
我的经验: 延迟低于100ms,用户基本无感;100-300ms,可以接受;超过300ms,对话就会开始别扭。我曾经优化过一个项目,把延迟从400ms降到了80ms,用户反馈直接翻了个倍。

四、压力测试:看看系统能扛多少人

压力测试,说白了就是模拟大量用户同时在线,看看你的服务器和客户端能不能扛住。

我常用的工具是k6artillery,但针对WebRTC,我更推荐自己写脚本,因为WebRTC的连接建立过程比较复杂。

4.1 模拟多用户并发

// stressTest.js
const puppeteer = require('puppeteer');

async function simulateUser(roomId) {
  const browser = await puppeteer.launch({ headless: true });
  const page = await browser.newPage();
  await page.goto(`http://localhost:3000/room/${roomId}`);
  await page.click('#join-btn');
  // 保持连接10秒
  await page.waitForTimeout(10000);
  await browser.close();
}

// 同时启动50个用户
const promises = [];
for (let i = 0; i < 50; i++) {
  promises.push(simulateUser('stress-test-room'));
}
await Promise.all(promises);

4.2 监控服务器资源

压力测试时,一定要同时监控服务器的CPU、内存、网络带宽。我一般用pidstatiftop

# 监控CPU和内存
pidstat -p $(pgrep -f signaling-server) 1

# 监控网络流量
iftop -i eth0

4.3 关键指标

指标 正常范围 告警阈值 说明
CPU使用率 < 70% > 90% 超过90%说明需要扩容
内存使用率 < 80% > 95% 内存泄漏是常见问题
连接建立成功率 > 99% < 95% 失败率过高说明信令或ICE有问题
平均延迟 < 150ms > 300ms 延迟飙升通常是带宽瓶颈
我曾经踩过的坑: 有一次压力测试,50个用户同时加入,服务器CPU直接飙到100%。排查了半天,发现是信令服务器的消息广播用了同步方式,导致事件循环被阻塞。改成异步队列后,问题解决了。所以压力测试一定要做,而且要早做。

知识体系总览

下面这张图,把WebRTC测试的四个维度串起来了。你可以看到,每个维度关注的点不同,但最终目标都是保证用户体验。

WebRTC测试知识体系 单元测试 (Jest) SDP解析函数测试 ICE状态机迁移测试 码率计算逻辑验证 → 保证函数逻辑正确 端到端测试 (Puppeteer) 模拟两个用户加入房间 验证连接建立成功 检查媒体流是否到达 → 模拟真实用户场景 媒体质量测试 PSNR / SSIM / VMAF 延迟测量 (RTT/端到端) 丢包率 / 抖动 / 帧率 → 客观量化用户体验 压力测试 模拟50+用户并发 监控CPU/内存/带宽 连接建立成功率 → 验证系统承载能力 目标:从代码到用户体验,全方位保障WebRTC质量

你看,这四个维度层层递进:单元测试守住代码底线,端到端测试验证功能完整性,媒体质量测试量化用户体验,压力测试确保系统稳定性。缺一个,你的WebRTC应用就可能在某一天突然翻车。

好了,今天就聊到这儿。测试这件事,投入越多,回报越大。别等到线上出问题了再后悔——嗯,我当年就是这么过来的。


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