首页
/ Create React App 测试指南:react-scripts 与 Jest 深度集成全解

Create React App 测试指南:react-scripts 与 Jest 深度集成全解

2026-09-04 20:43:45作者:魏侃纯Zoe

本文基于 create-react-app 仓库的官方文档 docusaurus/docs/running-tests.md 及其对应的实现源码,系统讲解 react-scripts test 背后的 Jest 集成机制:从测试文件命名约定、watch 模式、版本控制集成,到组件测试、测试环境初始化、覆盖率报告、可覆盖的 Jest 配置项以及 CI 场景下的运行方式。读完本文,你可以完整掌握 Create React App 项目从零编写测试到在持续集成中运行的全部实操路径,并理解每一条命令在源码中的落点。

为什么是 Jest:Node 环境中的测试运行器

Create React App 使用 Jest 作为测试运行器。Jest 是基于 Node 的 runner,这意味着测试始终运行在 Node 环境中而非真实浏览器里——这是它能提供快速迭代速度、避免测试不稳定性(flakiness)的关键。

虽然 Jest 通过 jsdom 提供了 window 等浏览器全局对象,但这些只是真实浏览器行为的近似。Jest 的定位是对你的业务逻辑和 React 组件做单元测试,而不是覆盖 DOM 的怪异行为。官方文档的建议是:如果需要浏览器端到端(E2E)测试,请使用独立工具,这超出了 Create React App 的职责范围。

从源码看,react-scripts 对 Jest 版本有明确约束。react-scripts/package.json 中声明了 "jest": "^27.4.3" 依赖,测试环境相关的 polyfill 由独立的 react-app-polyfill 包提供(见 packages/react-app-polyfill/jsdom.js)。

测试文件命名约定

Jest 会按以下流行命名约定查找测试文件:

  • __tests__ 文件夹中的 .js 文件;
  • .test.js 后缀的文件;
  • .spec.js 后缀的文件。

.test.js / .spec.js 文件(或 __tests__ 文件夹)可以位于 src 顶层文件夹下的任意深度。文档推荐将测试文件(或 __tests__ 文件夹)与被测代码放在同一目录,这样相对导入路径更短:例如 App.test.jsApp.js 同目录时,测试只需 import App from './App'。就近放置(collocation)也有助于在大型项目中更快定位测试。

这一约定并非 Jest 的默认行为,而是 Create React App 显式配置的。createJestConfig.js 中的 testMatch 精确限定了搜索范围只覆盖 src 下的四种扩展名:

testMatch: [
  '<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
  '<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}',
],

同时 roots 被设置为 ['<rootDir>/src'],即只有 src 目录下的文件才会被扫描为测试候选。这也是为什么 create-react-app 官方仓库自身的集成测试要单独放一份配置(见 test/jest.config.js,其 testPathIgnorePatterns 恰好排除了 /src/),以和项目模板的测试规则区分开。

命令行接口与 Watch 模式

运行 npm test 时,Jest 以 watch 模式启动:每次保存文件都会重新运行测试,类似 npm start 重新编译代码。

watcher 自带交互式命令行界面,可以运行全部测试,也可以聚焦到某个搜索模式。这种设计让你可以一直开着终端,享受快速的重跑体验。具体的按键命令可以查看每次运行后 watcher 打印的 "Watch Usage" 提示。

值得注意的是,非 CI 环境下 watch 模式有两种形态,scripts/test.js 中的逻辑决定了最终走哪一种:

// Watch unless on CI or explicitly running all tests
if (
  !process.env.CI &&
  argv.indexOf('--watchAll') === -1 &&
  argv.indexOf('--watchAll=false') === -1
) {
  const hasSourceControl = isInGitRepository() || isInMercurialRepository();
  argv.push(hasSourceControl ? '--watch' : '--watchAll');
}

即在 Git 或 Mercurial 仓库中注入 --watch(只运行与改动文件相关的测试),否则注入 --watchAll(运行全部测试)。虽然推荐开发时使用 watch 模式,但也可以传 --watchAll=false 显式关闭。在多数 CI 环境中这一点会被自动处理(见下文 "On CI servers")。

从源码还能看到两项提升交互体验的默认配置(createJestConfig.js):

watchPlugins: [
  'jest-watch-typeahead/filename',
  'jest-watch-typeahead/testname',
],

jest-watch-typeahead 插件支持在 watch 输入框中直接按文件名或测试名筛选运行,依赖声明见 react-scripts/package.json

版本控制集成

默认情况下,npm test 只运行自上次提交以来改动文件相关的测试。这是一个优化,目的是无论你有多少测试都能保持快速。其前提是:你不太经常提交不通过的代码。

Jest 会明确提示它只运行了与改动文件相关的测试;也可以在 watch 模式下按 a 强制运行全部测试。在持续集成服务器上,或者当项目不在 Git / Mercurial 仓库内时,Jest 总会运行全部测试。

scripts/test.js 通过两个子命令探测版本控制环境:

function isInGitRepository() {
  try {
    execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
    return true;
  } catch (e) {
    return false;
  }
}

function isInMercurialRepository() {
  try {
    execSync('hg --cwd . root', { stdio: 'ignore' });
    return true;
  } catch (e) {
    return false;
  }
}

编写测试

创建测试的方式是添加 it()(或 test())块,包含测试名和测试代码。可以可选地用 describe() 块做逻辑分组,但这既非必需也不被特别推荐。

Jest 内置了 expect() 全局函数用于断言。一个基础测试如下:

import sum from './sum';

it('sums numbers', () => {
  expect(sum(1, 2)).toEqual(3);
  expect(sum(2, 2)).toEqual(4);
});

Jest 支持的所有 expect() matcher 均有详尽的官方文档可查。你也可以用 jest.fn()expect(fn).toBeCalled() 创建 "spies"(间谍函数)或 mock 函数。

测试组件

组件测试技术有一个广泛的谱系:从验证组件渲染不抛错的 "smoke test",到浅渲染并断言部分输出,再到完整渲染并测试组件生命周期与状态变化。不同项目会根据组件变化频率和逻辑含量选择不同的测试权衡。官方文档建议:如果还没有确定测试策略,先从为组件编写基础 smoke test 开始:

import React from 'react';
import ReactDOMClient from 'react-dom/client';
import App from './App';

it('renders without crashing', () => {
  const div = document.createElement('div');
  ReactDOMClient.createRoot(div).render(<App />);
});

这个测试挂载了一个组件,并确保它在渲染期间没有抛错。这类测试投入很小、价值很大,是很好的起点。

值得对照的是,当前仓库中 CRA 官方模板 src/App.test.js 的写法已经演进为使用 React Testing Library(见下文),而不是上面的 createRoot 冒烟写法。cra-template 模板的 App.test.js 全文如下:

import { render, screen } from '@testing-library/react';
import App from './App';

test('renders learn react link', () => {
  render(<App />);
  const linkElement = screen.getByText(/learn react/i);
  expect(linkElement).toBeInTheDocument();
});

当遇到由组件变化引发的 bug 时,你会对哪些部分值得测试有更深的认识。那可能是一个引入更具体测试、断言特定输出或行为的好时机。

React Testing Library

如果想以与被测组件渲染的子组件相隔离的方式测试组件,推荐使用 react-testing-library。它是按照最终用户实际使用组件的方式来测试 React 组件的库,适合单元测试、集成测试和端到端测试。它直接操作 DOM 节点,因此建议搭配 jest-dom 使用以获得更好的断言能力。

安装 react-testing-libraryjest-dom

npm install --save @testing-library/react @testing-library/dom @testing-library/jest-dom

或者使用 yarn:

yarn add @testing-library/react @testing-library/dom @testing-library/jest-dom

如果不想在每个测试文件里写样板代码,可以创建 src/setupTests.js(见下文 "Initializing Test Environment"):

// react-testing-library renders your components to document.body,
// this adds jest-dom's custom assertions
import '@testing-library/jest-dom';

这正是官方模板的做法,cra-template/template/src/setupTests.js 内容与上面完全一致,并附带了使用示例注释(如 expect(element).toHaveTextContent(/react/i))。

react-testing-libraryjest-dom 配合测试 <App /> 渲染出 "Learn React" 的示例:

import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';

it('renders welcome message', () => {
  render(<App />);
  expect(screen.getByText('Learn React')).toBeInTheDocument();
});

react-testing-library 还提供了一系列用于测试异步交互和选择表单元素的工具,可在其官方文档与示例集合中进一步学习。

使用第三方断言库

官方建议优先使用 expect() 做断言、jest.fn() 做 spies。如果遇到问题,应该向 Jest 项目本身提 issue,官方有意持续改进它们在 React 场景下的表现(例如支持把 React 元素 pretty-print 成 JSX)。

不过,如果你熟悉 Chai、Sinon 等其他库,或者有需要迁移的既有代码,可以像平常一样正常导入它们:

import sinon from 'sinon';
import { expect } from 'chai';

然后在测试中照常使用即可。

初始化测试环境(setupTests.js)

注意:此功能需要 react-scripts@0.4.0 及以上版本。

如果你的应用使用了需要在测试中 mock 的浏览器 API,或者需要在运行测试前做全局初始化,请在项目中添加 src/setupTests.js,它会在测试运行前自动被执行。例如 mock localStorage

// src/setupTests.js
const localStorageMock = {
  getItem: jest.fn(),
  setItem: jest.fn(),
  removeItem: jest.fn(),
  clear: jest.fn(),
};
global.localStorage = localStorageMock;

这个 "自动执行" 并非魔法。实现链路如下:

  1. config/paths.js 通过 resolveModule 解析出 src/setupTests 模块(支持任意扩展名),赋给 paths.testsSetup
  2. createJestConfig.js 从该路径的正则匹配中提取扩展名,仅在文件真实存在时注入配置:
const setupTestsMatches = paths.testsSetup.match(/src[/\\]setupTests\.(.+)/);
const setupTestsFileExtension =
  (setupTestsMatches && setupTestsMatches[1]) || 'js';
const setupTestsFile = fs.existsSync(paths.testsSetup)
  ? `<rootDir>/src/setupTests.${setupTestsFileExtension}`
  : undefined;
// ...
setupFilesAfterEnv: setupTestsFile ? [setupTestsFile] : [],

注意:如果你在创建 src/setupTests.js 之前就执行了 eject,生成的 package.json 不会包含对它的引用。此时你需要在 Jest 配置中手动添加 setupFilesAfterEnv 属性:

"jest": {
  // ...
  "setupFilesAfterEnv": ["<rootDir>/src/setupTests.js"]
}

另外,createJestConfig.js 还专门做了防御:如果检测到你在 package.json 的 Jest 配置里写了 setupFilesAfterEnv,会打印红色错误提示"把它移除,把初始化代码放进 src/setupTests.js"并直接 process.exit(1) 终止——因为该文件本来就会被自动加载,重复配置反而埋坑。

聚焦与排除测试

  • it() 替换为 xit() 可以临时排除某个测试,使其不被执行;
  • fit() 则用于聚焦某个特定测试,跳过其他所有测试。

覆盖率报告

Jest 自带与 ES6 配合良好、无需额外配置的覆盖率报告器。运行:

npm test -- --coverage

注意中间多出来的 --,用于把 --coverage 透传给 Jest 而非 npm。该命令会输出类似终端文本格式的覆盖率报告。由于开启覆盖率后测试速度会明显变慢,建议将其与日常开发流程分开运行。

默认的收集范围在 createJestConfig.js 中已经给出:

collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts'],

即默认统计 src 下所有 JS/TS 源文件,并排除 .d.ts 声明文件。

覆盖默认 Jest 配置

Create React App 使用的 Jest 默认配置(即上文 createJestConfig.js 中构建的对象)可以通过在 package.json 中添加一个 jest 字段来覆盖,但仅限以下受支持的键:

  • clearMocks
  • collectCoverageFrom
  • coveragePathIgnorePatterns
  • coverageReporters
  • coverageThreshold
  • displayName
  • extraGlobals
  • globalSetup
  • globalTeardown
  • moduleNameMapper
  • resetMocks
  • resetModules
  • restoreMocks
  • snapshotSerializers
  • testMatch
  • transform
  • transformIgnorePatterns
  • watchPathIgnorePatterns

package.json 示例:

{
  "name": "your-package",
  "jest": {
    "collectCoverageFrom": [
      "src/**/*.{js,jsx,ts,tsx}",
      "!<rootDir>/node_modules/",
      "!<rootDir>/path/to/dir/"
    ],
    "coverageThreshold": {
      "global": {
        "branches": 90,
        "functions": 90,
        "lines": 90,
        "statements": 90
      }
    },
    "coverageReporters": ["text"],
    "snapshotSerializers": ["my-serializer-module"]
  }
}

源码中可以看到这个覆盖机制的精确行为(createJestConfig.js):

  • 数组或原始类型的键被直接覆盖
  • 对象类型的键(如 moduleNameMapper)会被 Object.assign 合并扩展,保留默认值;
  • 出现白名单之外的键时,进程打印受支持键清单、列出不受支持的键,并提示 "如需覆盖其他 Jest 选项,需要 eject",随后 process.exit(1)

也就是说,在不 eject 的前提下,你能安全调优的就是上面这张白名单,其他选项必须通过 npm run eject 获得完整控制。

持续集成(CI)

默认 npm test 会启动带交互式 CLI 的 watcher。设置环境变量 CI 后,可以强制 Jest 只运行一次测试并结束进程。

同理,npm run build 默认不检查 lint 警告;设置 CI 环境变量后,构建会执行 lint 警告检查,一旦遇到警告即失败。

主流 CI 服务器默认已设置 CI 变量,你也可以自行设置。

CI 服务器

Travis CI

  1. 参照 Travis 官方入门指南将 GitHub 仓库与 Travis 同步(可能需要在 profile 页面手动初始化部分设置)。
  2. 在 git 仓库中添加 .travis.yml
language: node_js
node_js:
  - 8
cache:
  directories:
    - node_modules
script:
  - npm run build
  - npm test
  1. 通过一次 git push 触发首次构建。
  2. 按需定制 Travis CI 构建。

CircleCI

参照社区文章设置 CircleCI 与 Create React App 项目的集成(原文档引用了相应的 Medium 教程)。

在自己环境中

Windows(cmd.exe)

set CI=true&&npm test
set CI=true&&npm run build

(注意:命令中缺少空格是有意为之。)

Windows(PowerShell)

($env:CI = "true") -and (npm test)
($env:CI = "true") -and (npm run build)

Linux、macOS(Bash)

CI=true npm test
CI=true npm run build

test 命令会强制 Jest 以 CI 模式运行,测试只跑一次而不启动 watcher;build 命令会检查 lint 警告,发现即失败。对于非 CI 环境,可以传 --watchAll=false 标志关闭测试 watch。

回看 scripts/test.js 的 watch 判定逻辑,!process.env.CI 是第一个短路条件——只要 CI 存在,无论是否在 git 仓库中,都不会注入 --watch / --watchAll,Jest 由此进入单次运行模式。

禁用 jsdom

如果你确定没有任何测试依赖 jsdom,可以安全地设置 --env=node,测试运行会更快:

  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
-   "test": "react-scripts test"
+   "test": "react-scripts test --env=node"

默认环境是 jsdom:createJestConfig.js 中写死了 testEnvironment: 'jsdom'。为了让 --env 参数正确生效,scripts/test.js 还包含一段针对 Jest 自身环境解析缺陷的 "脏 workaround":它手动解析 jest-environment-${env}(或直接解析环境包名),然后把解析结果重新拼回 argv

let env = 'jsdom';
// ... 从 argv 中剥离 --env 参数 ...
const testEnvironment = resolvedEnv || env;
argv.push('--env', testEnvironment);

为了帮你判断是否需要 jsdom,文档列出了两类 API:

需要 jsdom 的 API:

  • 任何浏览器全局对象,如 windowdocument
  • ReactDOM.render()
  • TestUtils.renderIntoDocument()(前者的快捷方式);
  • Enzyme 中的 mount()
  • React Testing Library 中的 render()

不需要 jsdom 的 API:

  • TestUtils.createRenderer()(浅渲染);
  • Enzyme 中的 shallow()

最后,快照测试(snapshot testing)也不需要 jsdom。

快照测试

快照测试是 Jest 的一项特性:它自动生成组件的文本快照并保存到磁盘,一旦 UI 输出发生变化,你无需手写任何针对组件输出的断言就能得到通知。

编辑器集成

如果你使用 Visual Studio Code,有官方推荐的 Jest 扩展与 Create React App 开箱即用,提供了大量 IDE 级体验:内联显示测试运行状态及可能的失败信息、自动启动和停止 watcher、一键更新快照。

小结:测试体系在源码中的完整链路

把上文各节串起来,npm test 在 create-react-app 模板项目中的完整链路是:

  1. scripts/test.js 设置 BABEL_ENV/NODE_ENVtest,根据 CI--watchAll 与版本控制环境决定 watch 策略;
  2. createJestConfig.js 构建 Jest 配置:src 作用域的 testMatch、jsdom 环境、Babel 转换(config/jest/babelTransform.js 基于 babel-preset-react-app,并按 React 版本自动选择 automatic / classic JSX runtime)、CSS Modules 映射到 identity-obj-proxy、非源码资产走 fileTransform.js(其中 SVG 还能以 ReactComponent 形式导出);
  3. src/setupTests.js(若存在)经 setupFilesAfterEnv 自动加载;
  4. package.json 中的 jest 字段在白名单内做覆盖/合并,越界即报错退出;
  5. react-app-polyfill/jsdom 通过 setupFiles 为测试环境补齐 polyfill。

掌握这条链路后,无论是调整测试匹配规则、限制覆盖率范围,还是排查 "为什么我的测试没被运行",都可以在上述文件中找到确切依据。

登录后查看全文
热门项目推荐
相关项目推荐