26、WebRTC测试:单元测试框架、端到端测试、压力测试与性能基准

做WebRTC开发,最怕什么?我最怕的是:本地跑得好好的,一上生产就崩。音视频这东西,涉及网络、编解码、信令、ICE穿透……任何一个环节出问题,用户感知就是「卡顿」「黑屏」「断连」。所以,测试不是可选项,是必选项。

今天咱们就聊聊WebRTC的测试体系。我把它分成四个层次:单元测试、端到端测试、压力测试、性能基准。每个层次解决不同的问题,缺一不可。

核心观点: WebRTC测试不能只靠「人工点一点」。自动化测试体系,是保障实时通信质量的基石。

一、单元测试:把每个模块锁死

单元测试,说白了就是测试「最小的可测试单元」。在WebRTC里,这个单元可能是一个PeerConnection的状态机、一个SDP解析函数、或者一个ICE候选地址的排序逻辑。

我个人习惯用 Jest 做单元测试。为什么?因为WebRTC的很多逻辑是纯JavaScript,Jest的mock能力很强,可以模拟RTCPeerConnection、MediaStream这些浏览器API。

举个例子,测试一个SDP解析函数:

// sdpParser.test.js
const { parseSDP, getMediaTracks } = require('./sdpParser');

describe('SDP Parser', () => {
  test('should extract audio and video tracks from SDP', () => {
    const mockSDP = `v=0
o=- 123456 2 IN IP4 127.0.0.1
s=-
t=0 0
m=audio 49170 RTP/AVP 0
a=rtpmap:0 PCMU/8000
m=video 51372 RTP/AVP 96
a=rtpmap:96 H264/90000`;

    const result = getMediaTracks(mockSDP);
    expect(result.audio).toBe(true);
    expect(result.video).toBe(true);
  });

  test('should return false for missing video', () => {
    const sdpWithoutVideo = `v=0
o=- 123456 2 IN IP4 127.0.0.1
s=-
t=0 0
m=audio 49170 RTP/AVP 0`;

    const result = getMediaTracks(sdpWithoutVideo);
    expect(result.video).toBe(false);
  });
});

我在项目中遇到过一个问题:SDP里ice-ufrag字段拼写错误,导致ICE连接一直失败。单元测试一跑,立马暴露了。嗯,从那以后,所有SDP相关的解析函数我都加了单元测试。

小技巧:jest.fn() 模拟RTCPeerConnection的createOffer、setLocalDescription等方法。这样不用真的建立连接,就能测试信令交互逻辑。

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

单元测试只能保证「零件没问题」,但零件组装起来能不能跑?这就需要端到端测试了。

端到端测试,我推荐用 PuppeteerPlaywright。它们可以启动真实的浏览器,打开两个页面,一个作为发送端,一个作为接收端,然后验证视频流是否正常传输。

核心思路是这样的:

  1. 启动两个浏览器实例(或两个页面)
  2. 页面A加入房间,页面B加入同一个房间
  3. 页面A开始推送本地视频流
  4. 等待几秒,检查页面B是否收到了视频帧
  5. 断言:接收到的帧数 > 0

代码大概长这样:

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

describe('WebRTC End-to-End Test', () => {
  let browser, pageA, pageB;

  beforeAll(async () => {
    browser = await puppeteer.launch({ headless: true });
    pageA = await browser.newPage();
    pageB = await browser.newPage();
  });

  test('should establish video call between two peers', async () => {
    await pageA.goto('https://your-webrtc-app.com/room/123');
    await pageB.goto('https://your-webrtc-app.com/room/123');

    // 模拟用户点击「加入房间」
    await pageA.click('#join-btn');
    await pageB.click('#join-btn');

    // 等待ICE连接建立
    await pageA.waitForSelector('#remote-video', { timeout: 10000 });
    await pageB.waitForSelector('#remote-video', { timeout: 10000 });

    // 检查是否有视频帧
    const frameCount = await pageA.evaluate(() => {
      const video = document.getElementById('remote-video');
      return video.webkitDecodedFrameCount || 0;
    });

    expect(frameCount).toBeGreaterThan(0);
  }, 30000); // 超时设为30秒

  afterAll(async () => {
    await browser.close();
  });
});
注意: 端到端测试依赖真实的网络环境。如果测试服务器在国外,国内跑可能会超时。我建议在CI/CD里用Docker部署一个本地信令服务器,避免网络波动影响测试结果。

你想想看,如果没有端到端测试,你怎么知道信令服务器和客户端之间的协议是否一致?我曾经踩过一个坑:信令服务器返回的JSON字段名是camelCase,但客户端代码写的是snake_case。单元测试全过了,端到端一跑,直接报错。从那以后,我坚持每个PR必须跑一遍端到端测试。

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

WebRTC的压力测试,和普通HTTP压力测试完全不同。HTTP压测是「发请求、等响应」,WebRTC压测是「建立连接、维持流、模拟丢包」。说白了,更复杂。

我常用的工具是 K6 配合 WebRTC的k6扩展。K6本身是压测工具,加上WebRTC扩展后,可以模拟多个虚拟用户同时建立PeerConnection。

压力测试的核心指标:

指标 说明 健康阈值
并发连接数 同时建立的PeerConnection数量 根据服务器配置,一般建议 < 1000
连接成功率 成功建立连接的比例 > 99%
平均连接耗时 从发起连接到ICE完成的时间 < 5秒
CPU使用率 服务器端CPU占用 < 80%

一个简单的K6脚本示例:

// stress-test.js
import { check, sleep } from 'k6';
import { WebRTC } from 'k6/x/webrtc';

export const options = {
  stages: [
    { duration: '30s', target: 50 },  // 30秒内升到50并发
    { duration: '1m', target: 50 },   // 维持1分钟
    { duration: '30s', target: 0 },   // 30秒内降到0
  ],
};

export default function () {
  const peer = new WebRTC.PeerConnection({
    iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
  });

  const result = peer.connect('wss://your-signaling-server.com/ws');
  check(result, {
    'connection established': (r) => r.status === 'connected',
  });

  sleep(10);
  peer.close();
}
经验之谈: 压力测试时,别忘了监控STUN/TURN服务器的负载。很多系统瓶颈不在信令服务器,而在TURN中继。我曾经遇到一个项目,TURN服务器带宽打满,导致所有用户都连不上。后来加了带宽监控和自动扩容,才解决问题。

四、性能基准:量化每一次改动

性能基准测试,不是为了「通过/不通过」,而是为了「对比」。每次代码改动后,跑一遍基准测试,看看延迟、吞吐量、丢包率有没有恶化。

我常用的基准测试工具是 Google Chrome的chrome://webrtc-internals 配合 WebRTC Stats API。通过编程方式采集RTCStatsReport,然后输出到文件或数据库。

采集的关键指标:

  • 端到端延迟:从发送端采集到接收端播放的时间差
  • 丢包率:packetsLost / packetsReceived
  • 抖动:jitter值,单位毫秒
  • 编解码时间:encodeTime / decodeTime
  • 帧率:framesPerSecond

代码示例:

// benchmark.js
async function collectStats(peerConnection) {
  const stats = await peerConnection.getStats();
  const report = {};

  stats.forEach((stat) => {
    if (stat.type === 'inbound-rtp' && stat.kind === 'video') {
      report.packetsLost = stat.packetsLost;
      report.jitter = stat.jitter;
      report.framesDecoded = stat.framesDecoded;
    }
    if (stat.type === 'outbound-rtp' && stat.kind === 'video') {
      report.packetsSent = stat.packetsSent;
      report.encodeTime = stat.avgEncodeMs;
    }
  });

  return report;
}

// 每5秒采集一次
setInterval(async () => {
  const stats = await collectStats(peerConnection);
  console.log(JSON.stringify(stats));
}, 5000);
重要: 性能基准测试一定要在「受控环境」下运行。网络带宽、CPU负载、浏览器版本都要固定。否则数据波动太大,没法对比。

我个人习惯把基准测试结果存到InfluxDB,然后用Grafana画成折线图。每次提交代码后,自动跑一遍基准测试,如果延迟增加了20%以上,CI直接失败。这样能及时发现性能回退。

五、知识体系总览

下面这张图,是我对WebRTC测试体系的总结。你可以把它当作一个检查清单:

WebRTC 测试体系总览 单元测试 Jest · 模块级 · 纯逻辑 端到端测试 Puppeteer · 真实浏览器 · 流验证 压力测试 K6 · 并发连接 · 容量评估 性能基准 Stats API · 延迟/丢包 · 回归检测 从左到右:测试粒度从细到粗 从上到下:测试阶段从开发到上线 开发阶段 集成阶段 上线前

看到这张图,你应该能理解:单元测试保底,端到端测试保通,压力测试保量,性能基准保优。四个层次层层递进,缺一不可。

我的建议: 如果你刚开始搭建WebRTC测试体系,先从单元测试和端到端测试入手。这两个能覆盖80%的常见问题。压力测试和性能基准,等系统稳定了再补上。

好了,这一章的内容就到这里。测试是个慢功夫,但值得投入。你想想看,一个线上事故造成的损失,可能够你写一年的测试代码了。


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