Create React App 测试指南:react-scripts 与 Jest 深度集成全解
本文基于 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.js 与 App.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-library 与 jest-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-library 与 jest-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;
这个 "自动执行" 并非魔法。实现链路如下:
- config/paths.js 通过
resolveModule解析出src/setupTests模块(支持任意扩展名),赋给paths.testsSetup; - 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 字段来覆盖,但仅限以下受支持的键:
clearMockscollectCoverageFromcoveragePathIgnorePatternscoverageReporterscoverageThresholddisplayNameextraGlobalsglobalSetupglobalTeardownmoduleNameMapperresetMocksresetModulesrestoreMockssnapshotSerializerstestMatchtransformtransformIgnorePatternswatchPathIgnorePatterns
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
- 参照 Travis 官方入门指南将 GitHub 仓库与 Travis 同步(可能需要在 profile 页面手动初始化部分设置)。
- 在 git 仓库中添加
.travis.yml:
language: node_js
node_js:
- 8
cache:
directories:
- node_modules
script:
- npm run build
- npm test
- 通过一次 git push 触发首次构建。
- 按需定制 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:
- 任何浏览器全局对象,如
window、document; 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 模板项目中的完整链路是:
- scripts/test.js 设置
BABEL_ENV/NODE_ENV为test,根据CI、--watchAll与版本控制环境决定 watch 策略; - createJestConfig.js 构建 Jest 配置:
src作用域的testMatch、jsdom 环境、Babel 转换(config/jest/babelTransform.js 基于babel-preset-react-app,并按 React 版本自动选择automatic/classicJSX runtime)、CSS Modules 映射到identity-obj-proxy、非源码资产走 fileTransform.js(其中 SVG 还能以ReactComponent形式导出); src/setupTests.js(若存在)经setupFilesAfterEnv自动加载;package.json中的jest字段在白名单内做覆盖/合并,越界即报错退出;react-app-polyfill/jsdom通过setupFiles为测试环境补齐 polyfill。
掌握这条链路后,无论是调整测试匹配规则、限制覆盖率范围,还是排查 "为什么我的测试没被运行",都可以在上述文件中找到确切依据。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00