Skip to content

测试与质量保证

目的与范围

测试与质量保证系统通过两种互补的方法提供 Three.js 渲染正确性和 API 功能的自动化验证:基于 Puppeteer 的视觉回归测试和基于 QUnit 的单元测试。主要系统是一个端到端(E2E)截图测试框架,它在 WebGL 和 WebGPU 后端上对 460+ 示例进行无头 Chrome(SwiftShader)渲染捕获并比较截图。本文档解释 Puppeteer 测试基础设施、确定性渲染技术、像素比较算法、单元测试组织和持续集成并行化。

有关承载这些示例的示例浏览器,请参阅第 6.2 页。有关文档生成,请参阅第 6.3 页。


概览

测试基础设施由两个互补的系统组成:

  1. E2E 视觉回归测试 - 位于 test/e2e/puppeteer.js 的 Puppeteer 驱动系统,在无头 Chrome 中使用 SwiftShader 渲染示例,以 400×250 分辨率捕获截图,并对 examples/screenshots/ 中的参考图像进行逐像素比较
  2. 单元测试 - 位于 test/unit/ 的基于 QUnit 的测试套件,验证单个类和数学原语

E2E 系统检测由核心库代码、着色器实现或材质系统更改引起的渲染输出视觉回归。它从 examples/files.json 处理示例目录,并生成显示实际与预期输出及像素差异高亮的比较报告。

概览

测试基础设施由两个主要组件组成:

  1. E2E 视觉回归测试 - 基于 Puppeteer 的系统,在无头 Chrome 中渲染示例,捕获截图并与参考图像比较
  2. 单元测试 - 位于 test/unit/ 的基于 QUnit 的测试套件(有提及但非主要关注点)

E2E 系统设计用于捕捉代码更改引入的视觉回归,确保渲染输出在核心库代码、着色器和材质的更新中保持一致。

系统架构

SVG
100%

配置参数

测试系统通过 test/e2e/puppeteer.js94-111 定义的常量配置渲染和比较阈值:

参数用途行号
port1234createServer() 的 HTTP 服务器端口96
pixelThreshold0.1Image.compare() 的每像素颜色距离阈值97
maxDifferentPixels0.3%测试通过/失败的百分比阈值98
idleTime2 秒page.waitForNetworkIdle() 超时100
parseTime1 秒/MB额外延迟:page.pageSize * parseTime101
networkTimeout5 分钟page.goto() 和网络空闲的最大时间103
renderTimeout5 秒等待 window._renderFinished 标志的最大时间104
numAttempts2makeAttempt() 中失败前的重试次数105
numCIJobs5GitHub Actions 矩阵大小用于并行化106
width × height400 × 250截图逻辑像素尺寸108-109
viewScale2实际视口倍数:width * viewScale110
jpgQuality95Image.write() 的 JPEG 压缩质量111

例外列表

测试框架维护一个在自动化测试期间跳过的示例例外列表。这些分为几类:

SVG
100%

测试执行流程

SVG
100%

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.js301page.evaluateOnNewDocument() 求值的确定性替代:

  • Math._random() - 带种子的伪随机数生成器
  • performance._now() - 单调时间戳提供者

这些覆盖确保动画和随机值在测试运行中产生相同输出。

请求拦截

测试框架拦截 HTTP 请求以注入修补的 Three.js 构建:

SVG
100%

这使系统能够提供替换非确定性函数的修改构建,同时保持所有其他资源不变。

截图比较

图像比较算法

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 来发信号表示渲染完成。测试框架等待此标志:

SVG
100%

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 索引示例范围近似数量
00% - 20%~92 示例
120% - 40%~92 示例
240% - 60%~92 示例
360% - 80%~92 示例
480% - 100%~92 示例

这将约 460 个示例测试分布在 5 个工作进程中以加快总执行时间。

控制台与错误处理

测试框架监控浏览器控制台输出和页面错误:

控制台处理器

位于 test/e2e/puppeteer.js304-367page.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.js253errorMessagesCache 数组在重试尝试间去重消息。

响应处理器

位于 test/e2e/puppeteer.js369-380page.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):

SVG
100%

这处理来自时序问题或网络故障的瞬时失败。

命令行界面

测试模式(与参考比较)

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 页定义的示例系统结构:

组件路径用途代码引用
示例 HTMLexamples/{name}.html单个示例页面通过 page.goto() 第 421 行导航
示例列表examples/files.jsonJSON 数组: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 系统通过验证无法孤立测试的集成渲染行为来补充单元测试。