29、端到端测试:使用 Selenium 和 WebRTC 测试框架编写端到端测试用例

端到端测试,说白了就是模拟真实用户的操作流程。你想想看,用户打开浏览器,点开你的视频通话页面,然后开始聊天——这一整套流程,我们得用代码自动跑一遍。

我刚开始做WebRTC测试时,总觉得单元测试够了。直到有一次上线后,发现两个Chrome版本之间居然连不上。嗯,从那以后,我再也不敢跳过端到端测试了。

为什么需要端到端测试?

WebRTC涉及的东西太多了。信令服务器、STUN/TURN、编解码器、网络协商……每个环节都可能出问题。单元测试只能测单个模块,集成测试能测模块间的交互,但只有端到端测试,才能验证整个系统在真实浏览器环境下的表现。

我个人习惯把端到端测试分成三层:

  • 基础连通性测试:两个浏览器能不能建立连接
  • 媒体流测试:音视频能不能正常传输
  • 异常场景测试:断网、切换设备、多人通话等

今天我们就重点讲第一层——用Selenium控制浏览器,配合WebRTC测试框架,写一个完整的端到端测试用例。

技术选型

做WebRTC端到端测试,常用的工具有这些:

工具 用途 我的评价
Selenium WebDriver 控制浏览器 稳定,社区大,必选
puppeteer 控制Chrome 轻量,但只支持Chromium
karma-webrtc WebRTC专用测试框架 适合单元测试,端到端不够灵活
testRTC 商业WebRTC测试平台 功能全,但收费

我推荐用 Selenium + 自定义WebRTC辅助库 的组合。Selenium负责控制浏览器,我们自己写一些辅助函数来操作WebRTC API。

核心架构

先看一张整体流程图,帮你理解测试是怎么跑起来的:

测试脚本 (Node.js + Mocha) Selenium WebDriver 浏览器 A (发起方) createOffer() 浏览器 B (接收方) createAnswer() 信令服务器 (WebSocket) SDP 交换 测试流程: 1. 测试脚本通过 Selenium 打开两个浏览器页面 2. 浏览器 A 创建 Offer,通过信令服务器发送给浏览器 B 3. 浏览器 B 创建 Answer,回传给浏览器 A 4. 验证 ICE 连接状态是否为 'connected'

环境准备

先把依赖装上。我用的是 Node.js + Mocha + Selenium WebDriver:

npm install selenium-webdriver mocha chai --save-dev

还需要下载对应浏览器的驱动。Chrome用chromedriver,Firefox用geckodriver。我个人习惯用Chrome,因为DevTools对WebRTC的支持更好。

小技巧: 用 headless 模式跑测试更快,但调试时建议关掉 headless,亲眼看看浏览器里发生了什么。

编写测试用例

好,开始写代码。我们先写一个最简单的测试:两个浏览器建立连接。

const { Builder, By, until } = require('selenium-webdriver');
const { expect } = require('chai');

describe('WebRTC 端到端测试', function() {
  // 设置超时时间,WebRTC连接可能需要几秒
  this.timeout(30000);

  let driverA, driverB;

  before(async function() {
    // 启动两个浏览器实例
    driverA = await new Builder().forBrowser('chrome').build();
    driverB = await new Builder().forBrowser('chrome').build();

    // 打开测试页面
    await driverA.get('http://localhost:3000/room/1');
    await driverB.get('http://localhost:3000/room/1');
  });

  after(async function() {
    await driverA.quit();
    await driverB.quit();
  });

  it('两个浏览器应成功建立 WebRTC 连接', async function() {
    // 等待连接状态变为 connected
    const statusA = await driverA.wait(
      until.elementLocated(By.id('connection-status')),
      10000
    );
    const textA = await statusA.getText();
    expect(textA).to.equal('connected');

    const statusB = await driverB.wait(
      until.elementLocated(By.id('connection-status')),
      10000
    );
    const textB = await statusB.getText();
    expect(textB).to.equal('connected');
  });
});

这段代码看起来简单,但实际跑起来坑不少。我曾经遇到过一个问题:两个浏览器打开同一个房间,但就是连不上。后来发现是信令服务器的WebSocket连接没处理好,第二个浏览器加入时没触发重新协商。

深入测试:验证媒体流

光看连接状态还不够。连接上了,但有没有视频流?这才是关键。

我一般会在测试页面里加一个辅助函数,用来检查视频元素是否有实际数据:

// 在测试页面中注入的辅助函数
window.checkVideoPlaying = function(videoElement) {
  return new Promise((resolve) => {
    if (videoElement.readyState >= 2) {
      resolve(true);
      return;
    }
    videoElement.addEventListener('canplay', () => resolve(true));
    // 超时处理
    setTimeout(() => resolve(false), 5000);
  });
};

然后在测试脚本里调用:

it('视频流应正常播放', async function() {
  // 在浏览器A中执行检查
  const isPlayingA = await driverA.executeScript(
    'return window.checkVideoPlaying(document.getElementById("remote-video"));'
  );
  expect(isPlayingA).to.be.true;

  // 在浏览器B中执行检查
  const isPlayingB = await driverB.executeScript(
    'return window.checkVideoPlaying(document.getElementById("remote-video"));'
  );
  expect(isPlayingB).to.be.true;
});
注意: 在 headless 模式下,浏览器可能没有真实的摄像头设备。你需要用假视频源(fake video track)来模拟。Chrome 启动参数加 --use-fake-device-for-media-stream 就能搞定。

处理 ICE 候选

WebRTC 连接过程中,ICE 候选的收集和交换是个关键环节。我习惯在测试中暴露 ICE 状态,方便断言:

it('ICE 连接状态应为 connected', async function() {
  const iceStateA = await driverA.executeScript(
    'return window.peerConnection.iceConnectionState;'
  );
  expect(iceStateA).to.equal('connected');

  const iceStateB = await driverB.executeScript(
    'return window.peerConnection.iceConnectionState;'
  );
  expect(iceStateB).to.equal('connected');
});

你想想看,如果 ICE 状态一直是 checking 或者 failed,那肯定是网络或者 STUN/TURN 配置有问题。我在项目中遇到过好几次,都是因为 TURN 服务器地址写错了。

模拟网络变化

端到端测试不能只测理想情况。真实场景下,网络会抖动、会断连。Selenium 可以配合 Chrome DevTools Protocol 来模拟网络条件:

it('网络切换时应能重新连接', async function() {
  // 模拟网络断开
  await driverA.executeCdpCommand('Network.emulateNetworkConditions', {
    offline: true,
    latency: 0,
    downloadThroughput: 0,
    uploadThroughput: 0
  });

  // 等待几秒
  await driverA.sleep(3000);

  // 恢复网络
  await driverA.executeCdpCommand('Network.emulateNetworkConditions', {
    offline: false,
    latency: 0,
    downloadThroughput: -1,
    uploadThroughput: -1
  });

  // 验证重新连接
  const iceState = await driverA.wait(
    async () => {
      const state = await driverA.executeScript(
        'return window.peerConnection.iceConnectionState;'
      );
      return state === 'connected' ? state : null;
    },
    15000,
    '连接未恢复'
  );
  expect(iceState).to.equal('connected');
});
核心要点: 端到端测试不是跑一遍就完事了。我建议把它集成到 CI/CD 流水线中,每次提交代码都自动跑一遍。这样能尽早发现问题,而不是等到上线后用户来报 bug。

测试报告与日志

测试跑完了,结果怎么看?我一般会做两件事:

  • 截图:在关键步骤截图,比如连接建立后、视频播放后。Selenium 的 takeScreenshot() 方法就能搞定。
  • 抓取 WebRTC 统计信息:通过 getStats() API 获取连接质量数据,写入日志文件。
// 截图示例
await driverA.takeScreenshot().then((data) => {
  require('fs').writeFileSync('screenshot.png', data, 'base64');
});

// 获取统计信息
const stats = await driverA.executeScript(
  'return window.peerConnection.getStats(null).then(r => { let res = {}; r.forEach(s => { res[s.type] = s; }); return res; });'
);
console.log('WebRTC Stats:', JSON.stringify(stats, null, 2));

说实话,WebRTC 的端到端测试确实比普通 Web 应用要复杂。但只要你把基础框架搭好,后面加测试用例就轻松多了。我个人习惯是先写一个最小可用的测试,跑通了再慢慢加各种场景。

好了,今天就聊到这里。记住一点:端到端测试不是银弹,但它能帮你抓住那些单元测试和集成测试漏掉的真实环境问题。下次遇到用户说「视频连不上」,你先跑一遍端到端测试,八成能找到原因。


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