测试与质量保证
目的与范围
测试与质量保证系统通过两种互补的方法提供 Three.js 渲染正确性和 API 功能的自动化验证:基于 Puppeteer 的视觉回归测试和基于 QUnit 的单元测试。主要系统是一个端到端(E2E)截图测试框架,它在 WebGL 和 WebGPU 后端上对 460+ 示例进行无头 Chrome(SwiftShader)渲染捕获并比较截图。本文档解释 Puppeteer 测试基础设施、确定性渲染技术、像素比较算法、单元测试组织和持续集成并行化。
有关承载这些示例的示例浏览器,请参阅第 6.2 页。有关文档生成,请参阅第 6.3 页。
概览
测试基础设施由两个互补的系统组成:
- E2E 视觉回归测试 - 位于 test/e2e/puppeteer.js 的 Puppeteer 驱动系统,在无头 Chrome 中使用 SwiftShader 渲染示例,以 400×250 分辨率捕获截图,并对 examples/screenshots/ 中的参考图像进行逐像素比较
- 单元测试 - 位于 test/unit/ 的基于 QUnit 的测试套件,验证单个类和数学原语
E2E 系统检测由核心库代码、着色器实现或材质系统更改引起的渲染输出视觉回归。它从 examples/files.json 处理示例目录,并生成显示实际与预期输出及像素差异高亮的比较报告。
概览
测试基础设施由两个主要组件组成:
- E2E 视觉回归测试 - 基于 Puppeteer 的系统,在无头 Chrome 中渲染示例,捕获截图并与参考图像比较
- 单元测试 - 位于
test/unit/的基于 QUnit 的测试套件(有提及但非主要关注点)
E2E 系统设计用于捕捉代码更改引入的视觉回归,确保渲染输出在核心库代码、着色器和材质的更新中保持一致。
系统架构
配置参数
测试系统通过 test/e2e/puppeteer.js94-111 定义的常量配置渲染和比较阈值:
| 参数 | 值 | 用途 | 行号 |
|---|---|---|---|
| port | 1234 | createServer() 的 HTTP 服务器端口 | 96 |
| pixelThreshold | 0.1 | Image.compare() 的每像素颜色距离阈值 | 97 |
| maxDifferentPixels | 0.3% | 测试通过/失败的百分比阈值 | 98 |
| idleTime | 2 秒 | page.waitForNetworkIdle() 超时 | 100 |
| parseTime | 1 秒/MB | 额外延迟:page.pageSize * parseTime | 101 |
| networkTimeout | 5 分钟 | page.goto() 和网络空闲的最大时间 | 103 |
| renderTimeout | 5 秒 | 等待 window._renderFinished 标志的最大时间 | 104 |
| numAttempts | 2 | makeAttempt() 中失败前的重试次数 | 105 |
| numCIJobs | 5 | GitHub Actions 矩阵大小用于并行化 | 106 |
| width × height | 400 × 250 | 截图逻辑像素尺寸 | 108-109 |
| viewScale | 2 | 实际视口倍数:width * viewScale | 110 |
| jpgQuality | 95 | Image.write() 的 JPEG 压缩质量 | 111 |
例外列表
测试框架维护一个在自动化测试期间跳过的示例例外列表。这些分为几类:
测试执行流程
Puppeteer 配置
测试框架启动 Chrome 时使用特定标志以确保一致渲染:
// 浏览器标志
const flags = [
'--hide-scrollbars',
'--use-angle=swiftshader', // 强制软件渲染
'--enable-unsafe-swiftshader',
'--no-sandbox'
];
const viewport = {
width: width * viewScale, // 800px
height: height * viewScale // 500px
};--use-angle=swiftshader 标志通过 SwiftShader 强制软件渲染,确保在不同硬件配置下输出一致,并消除 GPU 驱动差异。
确定性渲染
为确保可重复测试结果,系统注入覆盖非确定性 JavaScript API 的代码:
构建注入
buildInjection() 函数位于 test/e2e/puppeteer.js240,通过用确定性替代方案替换 Math.random() 调用来修补 Three.js 构建文件:
// 第 240 行
const buildInjection = (code) =>
code.replace(/Math.random() * 0xffffffff/g, 'Math._random() * 0xffffffff');这针对 Object3D 和其他 Three.js 核心类中用于生成唯一 ID 的特定模式。修补后的构建存储在 builds 对象中:
// 第 245-249 行
const builds = {
'three.core.js': buildInjection(await fs.readFile('build/three.core.js', 'utf8')),
'three.module.js': buildInjection(await fs.readFile('build/three.module.js', 'utf8')),
'three.webgpu.js': buildInjection(await fs.readFile('build/three.webgpu.js', 'utf8'))
};注入脚本
test/e2e/deterministic-injection.js 文件提供通过 test/e2e/puppeteer.js301 的 page.evaluateOnNewDocument() 求值的确定性替代:
Math._random()- 带种子的伪随机数生成器performance._now()- 单调时间戳提供者
这些覆盖确保动画和随机值在测试运行中产生相同输出。
请求拦截
测试框架拦截 HTTP 请求以注入修补的 Three.js 构建:
这使系统能够提供替换非确定性函数的修改构建,同时保持所有其他资源不变。
截图比较
图像比较算法
Image.compare() 方法逐像素比较两个截图:
| 步骤 | 操作 |
|---|---|
| 1. 尺寸检查 | 验证尺寸匹配 |
| 2. 像素迭代 | 遍历所有像素 |
| 3. 颜色距离 | 计算每通道差异 |
| 4. 阈值测试 | 检查距离是否 > pixelThreshold |
| 5. 差异标记 | 在输出中标记不同像素 |
| 6. 计数 | 返回总不同像素数 |
比较逻辑
位于 test/e2e/puppeteer.js519-552 的比较逻辑计算像素差异百分比:
// 第 527 行
const numDifferentPixels = expected.compare(screenshot, diff, pixelThreshold);
// 第 539 行
const differentPixels = numDifferentPixels / (actual.width * actual.height) * 100;
// 第 541 行
if (differentPixels < maxDifferentPixels) {
console.green(`Diff ${differentPixels.toFixed(1)}% in file: ${file}`);
} else {
// 第 547-550 行:写入三个输出文件用于调试
await screenshot.write(`test/e2e/output-screenshots/${file}-actual.jpg`, jpgQuality);
await expected.write(`test/e2e/output-screenshots/${file}-expected.jpg`, jpgQuality);
await diff.write(`test/e2e/output-screenshots/${file}-diff.jpg`, jpgQuality);
throw new Error(`Diff wrong in ${differentPixels.toFixed(1)}% of pixels`);
}渲染等待循环
示例必须通过设置 window._renderFinished = true 来发信号表示渲染完成。测试框架等待此标志:
CI 并行化
GitHub Actions 在 5 个并行作业中运行测试套件。每个作业处理一部分示例:
if ('CI' in process.env) {
const CI = parseInt(process.env.CI);
files = files.slice(
Math.floor(CI * files.length / numCIJobs),
Math.floor((CI + 1) * files.length / numCIJobs)
);
}作业分布
| CI 索引 | 示例范围 | 近似数量 |
|---|---|---|
| 0 | 0% - 20% | ~92 示例 |
| 1 | 20% - 40% | ~92 示例 |
| 2 | 40% - 60% | ~92 示例 |
| 3 | 60% - 80% | ~92 示例 |
| 4 | 80% - 100% | ~92 示例 |
这将约 460 个示例测试分布在 5 个工作进程中以加快总执行时间。
控制台与错误处理
测试框架监控浏览器控制台输出和页面错误:
控制台处理器
位于 test/e2e/puppeteer.js304-367 的 page.on('console') 处理器捕获浏览器控制台输出:
// 第 304-320 行:提取控制台参数
page.on('console', async msg => {
const type = msg.type();
const args = await Promise.all(msg.args().map(async arg => {
return await arg.executionContext().evaluate(
arg => arg instanceof Error ? arg.message : arg,
arg
);
}));
let text = args.join(' ');
text = file + ': ' + text.replace(/[.WebGL-(.+?)] /g, '');
// 第 353-359 行:处理错误类型
if (type === 'error') {
page.error = text; // 标记页面为失败
}
});位于 test/e2e/puppeteer.js253 的 errorMessagesCache 数组在重试尝试间去重消息。
响应处理器
位于 test/e2e/puppeteer.js369-380 的 page.on('response') 处理器累积页面大小以计算解析时间:
// 第 369-376 行
page.on('response', async (response) => {
if (response.status === 200) {
await response.buffer().then(buffer => page.pageSize += buffer.length);
}
});
// 在第 475 行使用:parseTime = page.pageSize / 1024 / 1024 * parseTime * 1000重试逻辑
每个示例尝试最多可重试 numAttempts 次(默认:2):
这处理来自时序问题或网络故障的瞬时失败。
命令行界面
测试模式(与参考比较)
npm run test-e2e # 测试所有非例外示例
npm run test-e2e --webgpu # 筛选到 webgpu_* 示例(第 203 行)
npm run test-e2e webgl_animation_keyframes # 按名称测试特定示例
npm run test-e2e file1 file2 file3 # 测试多个特定示例制作模式(生成参考截图)
npm run make-screenshot file1 file2 # 生成新的参考 JPG
npm run make-screenshot --webgpu file1 # 生成 WebGPU 参考位于 test/e2e/puppeteer.js164-180 的参数解析处理 --webgpu 和 --make 标志:
- 第 165-169 行:
--webgpu标志检测 - 第 172-176 行:
--make标志设置isMakeScreenshot = true - 第 179-180 行:剩余参数是示例名称的
exactList
输出与报告
成功输出
✓ Diff 0.1% in file: webgl_animation_keyframes
✓ Screenshot generated for file: webgl_animation_keyframes
✓ TEST PASSED! 220 screenshots rendered correctly.失败输出
✗ Diff wrong in 1.5% of pixels in file: webgl_materials_physical_transmission
✗ List of failed screenshots: webgl_materials_physical_transmission
✗ If you are sure that everything is correct, try to run "npm run make-screenshot webgl_materials_physical_transmission"
✗ TEST FAILED! 1 from 220 screenshots have not rendered correctly.失败的测试生成三张输出图像:
{file}-actual.jpg- 当前渲染输出{file}-expected.jpg- 参考截图{file}-diff.jpg- 突出显示差异的视觉差异图
与示例系统的集成
测试基础设施依赖于第 6.2 页定义的示例系统结构:
| 组件 | 路径 | 用途 | 代码引用 |
|---|---|---|---|
| 示例 HTML | examples/{name}.html | 单个示例页面 | 通过 page.goto() 第 421 行导航 |
| 示例列表 | examples/files.json | JSON 数组:webgl[], webgpu[], 等 | 通过 fs.readdir('examples') 第 184 行读取 |
| 示例标签 | examples/tags.json | 功能分类和搜索元数据 | 测试框架未使用 |
| 截图 | examples/screenshots/{name}.jpg | 比较用的参考图像 | 通过 Image.read() 第 511 行读取 |
| 构建文件 | build/three.core.js build/three.module.js build/three.webgpu.js | 库分发 | 修补和拦截 第 245-249, 383-405 行 |
测试系统构造 URL 为 http://localhost:${port}/examples/${file}.html 并拦截对 /build/three.*.js 的请求以注入确定性补丁。
单元测试
单元测试位于 test/unit/,使用 QUnit 作为测试框架。这些测试关注:
- 数学原语(Vector3, Matrix4, Quaternion)
- 核心类功能
- API 契约验证
- 边界情况处理
E2E 系统通过验证无法孤立测试的集成渲染行为来补充单元测试。