测试体系

概述

ncc 使用 Jest 作为测试框架,测试分为三类:集成测试、CLI 测试和单元测试。测试默认运行 dist/ 中的编译产物,coverage 模式下运行 src/ 源码。

测试配置

// jest.config.js
module.exports = {
  collectCoverageFrom: ["src/**/*.js"],
  coverageReporters: ["html", "lcov"],
  testEnvironment: "node",
  testMatch: ["<rootDir>/test/**/*.test.js"]
};

运行命令需要特殊 Node.js 参数:

node --expose-gc --max_old_space_size=4096 node_modules/jest/bin/jest.js
  • --expose-gc:允许测试手动触发 GC(global.gc()),避免内存积累
  • --max_old_space_size=4096:部分测试包(如 twilio)编译时内存开销大

集成测试

文件结构

test/
├── integration.test.js      # 集成测试运行器
└── integration/
    ├── express.js           # 测试编译 express
    ├── mongoose.js          # 测试编译 mongoose
    ├── pg.js                # 测试编译 pg
    └── ...                  # 约 70+ 个包测试

测试运行器 (test/integration.test.js)

// test/integration.test.js:42-83
for (const integrationTest of fs.readdirSync(__dirname + "/integration")) {
  if (!/\.(mjs|tsx?|js)$/.test(integrationTest)) continue;
  // 跳过已知失败的测试...
  
  it(`should execute "ncc run ${integrationTest}"`, async () => {
    const stdout = new StoreStream();
    const stderr = new StoreStream();
    await nccRun(["run", "--no-cache", `${__dirname}/integration/${integrationTest}`], stdout, stderr);
    // 如果有 .stdout 文件,验证输出匹配
  });
}

每个集成测试文件是一个可执行的 Node.js 脚本,通过 ncc run --no-cache 编译并执行。测试通过的标准是进程正常退出(exit code 0)。

平台跳过规则

平台跳过的测试原因
Windowsbinary-require.jsbrowserify-middleware.jsoracledb.jstensorflow.js原生模块兼容问题
macOSleveldown.jsleveldown #801

额外测试

// test/integration.test.js:86-128
it('should execute "ncc build web-vitals" with target config', async () => { ... });
it('should correctly handle the "typeModule" build', async () => { ... });

验证特定功能:

  • --target es5 配合 web-vitals
  • "type": "module" 包的正确处理

CLI 测试

文件结构

test/
├── cli.js                   # 测试用例定义
└── cli.test.js              # 测试运行器

测试用例格式 (test/cli.js)

module.exports = [
  {
    args: ["run", "test/fixtures/ts-interop/interop.ts"],
    expect: { code: 0 }
  },
  {
    args: ["build", "-o", "tmp", "test/fixtures/test.cjs"],
    expect(code, stdout, stderr) {
      return stdout.toString().indexOf(join('tmp', 'index.cjs')) !== -1;
    }
  },
  {
    args: ["build", "-o", "tmp", "--watch", "test/fixtures/no-dep.js"],
    timeout: 500,
    expect: { timeout: true }
  },
  // ...
];

每个用例可定义:

  • args:CLI 参数数组
  • env:环境变量覆盖
  • timeout:超时时间(用于 watch 模式测试)
  • expect:期望结果,支持三种形式:
    • { code: N }:期望退出码
    • { timeout: true }:期望超时
    • function(code, stdout, stderr, timedOut):自定义验证

测试运行器 (test/cli.test.js)

// test/cli.test.js:8-33
for (const cliTest of cliTests) {
  it(`should execute "ncc ${(cliTest.args || []).join(" ")}"`, async () => {
    const ps = fork(join(__dirname, file), cliTest.args || [], {
      stdio: "pipe",
      env: { ...process.env, ...cliTest.env },
    });
    // 收集 stdout/stderr,等待退出,验证期望
  });
}

CLI 测试通过 child_process.fork() 运行,每个测试是一个独立进程。

CLI 测试覆盖的场景

场景验证内容
TypeScript 互操作.ts 文件正确编译和执行
ESM 互操作.mjs 文件正确处理
输出文件名.cjs/.mjs 输出正确的扩展名
V8 缓存--v8-cache 正常工作
Source Map错误堆栈正确映射到源码
Watch 模式启动成功(验证超时)
TypeScript 错误报告正确的文件和行号
--transpile-only跳过类型错误
--quiet无输出
--stats-out生成有效 JSON
Source Map 路径路径正确映射
自定义 TypeScript 路径TYPESCRIPT_LOOKUP_PATH 环境变量

测试固件

test/fixtures/
├── error.js                    # 故意抛出错误的文件
├── no-dep.js                   # 无依赖的简单文件
├── test.cjs / test.mjs         # CJS/ESM 格式测试
├── module.cjs                  # CJS 模块(验证不输出 export)
├── interop-test.mjs            # ESM 互操作测试
├── type-module/                # "type": "module" 测试
├── ts-error1/ / ts-error2/     # TypeScript 错误测试
├── ts-interop/                 # TypeScript 互操作
├── with-type-errors/           # 带类型错误的 TS(transpileOnly)
└── sourcemap-resource-path/    # Source Map 路径测试

原生模块测试

test/binary/
├── binding.gyp        # node-gyp 配置
└── hello.cc           # C++ 原生模块源码

通过 pnpm build-test-binary 编译为 test/integration/hello.node,验证 ncc 能正确处理原生 .node 文件。

Coverage 模式

pnpm test-coverage

差异:

  • 使用 --runInBand(串行执行,确保覆盖率收集正确)
  • 设置全局变量 global.coverage = true
  • 测试运行器加载 src/cli.js 而非 dist/ncc/cli.js
  • 生成 HTML 和 LCOV 格式的覆盖率报告

测试超时

  • 集成测试:200 秒(jest.setTimeout(200000))— 大型包编译耗时
  • CLI 测试:20 秒(jest.setTimeout(20000)