午饭前,你提交了一个微小的 UI 改动。看起来毫无风险:改了个按钮标签,简化了一个条件渲染,又给一个 helper hook 增加了一个分支。Pull request 很干净,代码评审很快,部署顺利完成。
一小时后,客服反馈某个平台的登录功能失效了。Web 端看起来正常,但桌面端 shell 存在过时的渲染路径。移动端 build 在异步状态变更后表现异常。没人发现问题,因为 code 虽然有测试,但测试用例不对,更没有围绕这些测试建立起可靠的系统。
这就是生产团队在进行 React 单元测试时面临的核心问题。写几个通过的测试并不难,难的是构建一套能在重构、发布周期、热修复和跨平台打包过程中持续保护你的测试套件。React 应用崩溃通常不是因为团队忘记了如何调用 render()。它们崩溃是因为测试逐渐偏向于实现细节,异步行为被掩盖,而 CI 只是把测试当成一个勾选框,而不是一道发布关卡。
现代 React 单元测试应当像安全系统一样工作:本地提供快速反馈,CI 环境中进行确定性检查,并明确界定单元测试的边界。当同一套 React 代码需要分发到浏览器、Capacitor 容器或 Electron 外壳时,这一点尤为重要。
目录
- 为什么 React 单元测试是你的最佳安全网
- 搭建现代 React 测试环境
- 编写有意义的组件测试
- 测试自定义 Hooks 与应用逻辑
- 进阶技巧:Mock 与异步处理
- 提升测试质量与策略
- 将测试集成到跨平台 CI/CD Pipeline
为什么 React 单元测试是你的最佳安全网
当单元测试捕捉到你认为绝不会发生的错误时,它才真正发挥价值。在 React 中,这通常意味着组件仍能渲染,但用户依赖的行为发生了变化。例如:禁用的按钮变得可点击;加载状态永远无法清除;重构后回退消息消失了。这些故障在 code 中影响虽小,但在生产环境中代价高昂。
React 测试发生了一个重要的转变,那就是 React Testing Library 成为主流,开始测试行为而非内部实现这促使团队转向模拟用户行为而非组件 props 或 state 的测试,正如 React Native 在以下文档中的测试指南所体现的: React Native 测试概览。这种转变非常重要,因为 React code 经常需要重构。Hooks 会移动,组件会拆分,Context 会被引入。如果测试与内部结构绑定,在正常的重构中就会崩溃;而如果测试与可见行为绑定,通常就能稳定运行。
单元测试应该保护什么
一个优秀的 React 单元测试保护的是一个微小的契约:
- 渲染输出: 用户看到的文本、标签、状态或回退内容是否正确?
- 交互行为: 点击、输入或切换操作是否正确改变了 UI?
- 边界处理: 组件在接收预期输入、缺失数据或进入错误路径时表现是否正常?
弱测试保护了错误的对象:
- 组件内部实现: State 结构、私有方法、仅限实现的 props
- 框架机制: React 是否以你预期的内部方式更新了 hook
- 子组件细节: 此处不应验证的嵌套组件所拥有的 Markup
实用原则: 如果你可以在不改变用户感知或操作的前提下重构组件,那么测试也无需随之更改。
单元测试也处于更广泛的测试体系中。它们的目标并非证明整个 App 的端到端运行,而是在需要浏览器级测试或设备级验证之前,通过快速响应层来捕获回归问题。这就是为什么在任何合理的测试栈中,它们都是第一道防线: 生产级应用的自动化测试.
对于频繁交付的 React 团队来说,信心源于这种分工:单元测试快速捕获局部回归;集成测试验证接口衔接;端到端测试确认核心路径。如果跳过单元测试层,下游所有更慢的测试环节都将承担过重的压力。
搭建现代 React 测试环境
在编写任何断言之前,脆弱的测试环境就会导致测试结果不稳定(flaky)。许多开发者会将问题归咎于 Jest、jsdom 或 React,但根本原因往往是本地机器与 CI 之间的配置不一致。解决办法是让环境变得“枯燥”。在这里,“枯燥”意味着稳定。

从可预测的 Runner 和环境开始
对于现代 React 应用(尤其是使用 Vite 创建的应用),基础配置应包括:
- 测试 Runner: Jest 仍然非常流行,尤其是在旧的 React 代码库和企业级 CI 栈中。
- 类浏览器环境:
jsdom让组件测试能够渲染 DOM 输出。 - Testing Library 工具库:
@testing-library/react和@testing-library/jest-dom - 单一的 setup 入口点: 通过一个文件来注册 matchers 和全局 mocks
React 测试指南强调的核心工作流非常简单:在 jsdom 环境中渲染组件,使用如下选择器查询 UI: getByText 或 getByRole,触发交互,并断言 DOM 的变化,正如 React 测试文档中所描述的那样。只有当每台机器都运行相同的测试环境时,该工作流才是可靠的。
一个实用的 Jest 配置通常如下所示:
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
moduleNameMapper: {
'\\.(css|less|scss)$': 'identity-obj-proxy',
'^@/(.*)$': '<rootDir>/src/$1',
},
transform: {
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
},
};
如果你的团队使用 SWC 而不是 Babel,也没关系。重点不在于转换器,而在于一致性。选择一种方案并在仓库中标准化。如果你想参考更广泛的 JavaScript 测试规范,Capgo 的 JavaScript 单元测试指南 是团队交接时非常有用的文档。
添加测试套件依赖的 setup 文件
一个完善的 setupTests.js 可以减少大量重复的冗余代码:
import '@testing-library/jest-dom';
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
在这个文件中,你可以一次性解决环境差异问题,而不是在二十个测试文件中重复操作。为 UI 依赖的 API 添加 mock,例如 matchMedia, ResizeObserver,或者 IntersectionObserver,如果你的组件库需要它们的话。
如果没有这个文件,开发者会临时修补全局变量。这会导致测试不一致且故障难以追踪。某人的本地运行通过了,因为他在某个文件中手动添加了 mock;但 CI 却失败了,因为 setup 配置没有共享。
保持本地与 CI 环境行为一致
本地命令应尽量与 CI 命令保持一致。如果开发者以宽松的默认配置运行 watch 模式,而 CI 运行更严格的配置,合并后就会遇到意外失败。脚本要写明确:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:ci": "jest --runInBand --coverage"
}
}
简短的上手说明能帮助新团队成员快速对齐基线:
最有影响力的配置选择是管好默认值。把 aliases 写进配置,把环境 mock 统一放在一个 setup 文件里。使用 jsdom 来跑 UI 测试,并尽可能为纯工具函数使用更轻量的环境。每个测试需要的自定义行为越少,系统就越可靠。
编写有意义的组件测试
组织并不缺写测试的能力,缺的是写出半年后依然有意义的测试。
React 组件单元测试的标准模式依然是正确的选择: 渲染组件,用面向用户的选择器查询 UI,触发交互,并断言由此产生的 DOM 变化,这能让测试避开 state 或 props 等实现细节,正如 React 测试指南中所描述的那样。诀窍在于有节制地应用这一模式。
像用户那样测试折叠面板
以一个基础 Accordion 组件为例。它渲染一个带有标题的按钮。面板内容初始状态为隐藏。点击按钮会显示内容并更新无障碍状态。
这些行为足够写几个有用的测试:
- 初始渲染会显示标题,但不显示内容。
- 点击触发器会显示内容。
- 再次点击会将其折叠。
- 无障碍属性反映可见状态。
最后一点经常被忽略。如果你的组件使用了 aria-expanded, aria-controls,或基于 role 的结构,请务必验证它们。这些并非实现细节,而是面向用户的契约。
最好的组件测试读起来就像一份你永远不想收到的 Bug 报告。
根据意图选择查询方式
React Testing Library 提供了多种查询风格,但它们不能互换。选错查询方式会让测试变得嘈杂或产生误导。
| 查询类型 | 当找到元素时 | 当未找到元素时 | 使用场景示例 |
|---|---|---|---|
getBy |
立即返回元素 | 立即抛出错误 | 断言按钮或标题应已在屏幕上 |
queryBy |
立即返回元素 | 返回 null |
断言交互前隐藏内容不应存在 |
findBy |
元素出现时 resolve | 等待后 reject | 断言 fetch 或延迟更新后异步加载的内容是否出现 |
一个简单的思维模型很有帮助:
- 使用
getBy用于必须已经存在的内容。 - 使用
queryBy用于目前还不存在的内容。 - 使用
findBy用于 UI 随后发生变化的情况。
如果一个测试以 findBy 开始,通常意味着作者不确定组件何时更新。这种不确定性随后会导致测试不稳定(flakiness)。
一个实用的 Accordion 示例
这是一个典型的组件:
function Accordion({ title, children }) {
const [open, setOpen] = React.useState(false);
return (
<section>
<button
aria-expanded={open}
aria-controls="accordion-panel"
onClick={() => setOpen(prev => !prev)}
>
{title}
</button>
{open ? (
<div id="accordion-panel">
{children}
</div>
) : null}
</section>
);
}
以下是值得保留的测试形态:
import { render, screen, fireEvent } from '@testing-library/react';
test('renders the accordion title and hides content initially', () => {
render(<Accordion title="Shipping details">Delivery takes 3 days</Accordion>);
expect(screen.getByRole('button', { name: /shipping details/i })).toBeInTheDocument();
expect(screen.queryByText(/delivery takes 3 days/i)).not.toBeInTheDocument();
});
test('reveals content when the trigger is clicked', () => {
render(<Accordion title="Shipping details">Delivery takes 3 days</Accordion>);
fireEvent.click(screen.getByRole('button', { name: /shipping details/i }));
expect(screen.getByText(/delivery takes 3 days/i)).toBeInTheDocument();
});
test('updates aria-expanded when opened', () => {
render(<Accordion title="Shipping details">Delivery takes 3 days</Accordion>);
const button = screen.getByRole('button', { name: /shipping details/i });
expect(button).toHaveAttribute('aria-expanded', 'false');
fireEvent.click(button);
expect(button).toHaveAttribute('aria-expanded', 'true');
});
缺失的部分同样重要。不要对内部状态进行断言,也不要检查 setOpen 是否被调用,或者对整个渲染树进行快照。这些测试只会增加维护成本,而无法提升信心。
以下习惯能让组件测试更强大:
- 优先使用基于 role 的查询: 按钮、标题、对话框、警报和输入框通常应该通过 role 来查找。
- 保持每个测试的范围精简: 每个测试只验证一个用户可见的行为,这样失败时的报错才易于阅读。
- 以结果来命名测试: “打开时更新 aria-expanded”比“运行正常”要有用得多。
如果一个组件很难通过 DOM 进行测试,这通常意味着设计存在问题。也许是状态隐藏在了错误的地方,或者缺乏语义化标记。优秀的测试往往能推动团队开发出更好的组件。
测试 Custom Hooks 和应用逻辑
React 应用的大量核心逻辑都隐藏在组件之外。状态转换存在于 hooks 中;校验和格式化逻辑存在于 helper 函数中;数据处理通常发生在渲染之前。如果你只测试可见组件,将会错过很大一部分可能导致生产环境故障的 code。
Hooks 需要一个感知 React 的测试框架
自定义 hook 仍需要 React 环境才能正常运行,因此请使用 renderHook 并将改变状态的调用封装在 act().
一个简单的 useToggle hook 是一个很好的例子:
import { useState, useCallback } from 'react';
export function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue(current => !current), []);
return { value, toggle };
}
其测试应专注于公开接口:
import { renderHook, act } from '@testing-library/react';
import { useToggle } from './useToggle';
test('returns the initial value', () => {
const { result } = renderHook(() => useToggle(true));
expect(result.current.value).toBe(true);
});
test('toggles the value', () => {
const { result } = renderHook(() => useToggle(false));
act(() => {
result.current.toggle();
});
expect(result.current.value).toBe(true);
});
该测试之所以有用,是因为 hook 本身就是单元。你不是在测试 React 内部机制,而是在验证 hook 的外部行为。
对于构建可复用 UI 或功能原语的产品团队来说,这种模式至关重要。Hooks 往往成为跨应用、设计系统或内部工具共享的接口。如果你正在设计具有商业价值的可复用行为,那么关于 面向开发者产品的 hooks 资源 可以帮助你将 hooks 视为产品化的构建模块,而非仅仅是实现细节。
纯逻辑在测试中应保持纯粹
并非所有内容都需要 jsdom、React 或 Testing Library。如果一个函数是纯函数,直接在 Node 环境中使用 Jest 进行测试即可。
示例:
export function formatDisplayName(firstName: string, lastName: string) {
return `${firstName.trim()} ${lastName.trim()}`.trim();
}
这样的测试应该非常简单:
import { formatDisplayName } from './formatDisplayName';
test('joins and trims both names', () => {
expect(formatDisplayName(' Ada ', ' Lovelace ')).toBe('Ada Lovelace');
});
test('handles a missing last name', () => {
expect(formatDisplayName('Ada', '')).toBe('Ada');
});
这样做的好处是速度快且逻辑清晰。当一个函数不需要渲染树时,就不要给它提供渲染树。React 特有的工具链会增加额外开销。保持业务逻辑测试的轻量、快速,并尽可能贴近其验证的函数本身。
一种实用的拆分方式如下:
- Hooks: 使用
renderHook,act(),并在需要时使用 wrapper providers。 - 工具函数: 直接使用 Jest,无需 DOM。
- 带状态的跨切面逻辑: 当组件测试逻辑过于臃肿时,将其抽离到可测试的 helper 函数中。
团队经常在组件测试中堆砌本该属于底层逻辑的断言。将这些逻辑抽离有两个好处:组件测试更简洁,逻辑测试更快速。
掌握进阶技巧:Mock 与异步测试
大多数不可靠的 React 测试套件都会在两个地方崩溃:依赖边界和时间维度。
这就是为什么异步测试和 Mock 是区分“玩具级测试”与“发布前可信测试”的分水岭。一项分析指出, 46.5% 的测试不稳定性源于环境或资源相关问题,例如异步时序问题。 在 这项 React 单元测试分析。在 React 应用中,这直接对应于状态转换、延迟渲染、网络驱动的 UI,以及那些靠“猜”而非确定性等待的测试。

Mock 边界,而非每一层
编写误导性测试最快的方法,就是 Mock 掉半个组件树,然后去断言你自己的 Mock 是有效的。
对于获取账户数据的组件,请 mock 网络客户端或 API 模块。除非测试确实需要在这些边界实现隔离,否则不要去 mock hook、子行组件、loading spinner 以及三个工具函数。
请遵循以下规则集:
- Mock 外部服务: HTTP clients、analytics、仅限浏览器的 API、native bridges
- Mock 不稳定的平台 API:
matchMedia定时器、Electron preload 接口、在 jsdom 中不可用的 Capacitor plugins - 默认避免 mock 自身的内部实现: custom hooks、简单的子组件、本地 utilities
如果一个测试通过仅仅是因为所有难点都被 fake 替换了,那么它并不能为你提供多少发布信心。
对于想要了解 runner APIs 示例和模式的团队,Capgo 测试教程 是一个实用的参考库,特别是在为熟悉 React 但还不了解测试机制的开发者进行 onboarding 时。
异步测试在时序模糊时会失败
异步失败通常源于以下三种错误:
- 测试断言过早。
- 测试使用了不确定的定时器进行等待。
- 组件更新了多次,但测试仅模拟了一次状态转换。
一个稳定的异步测试通常如下所示:
test('shows user details after data loads', async () => {
render(<UserProfile userId="42" />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
expect(await screen.findByText(/account owner/i)).toBeInTheDocument();
});
或者,当你需要等待特定条件时:
await waitFor(() => {
expect(screen.getByRole('alert')).toBeInTheDocument();
});
使用 findBy 当某个元素的出现是你关注的事件时,使用 waitFor 当条件更宽泛或状态无法通过单一查询表达时,使用。避免使用 setTimeout 在测试中,除非你正在显式测试定时器行为并使用了 fake timers。
React 的测试生态系统也要求你遵循 act() 关于更新的语义。Testing Library 会帮你处理大部分工作,但如果你在手动驱动状态或推进定时器,仍需考虑更新何时刷新。
了解该选择哪种 Mock 工具
不同的 Mock 工具解决不同的问题:
| 工具 | 最佳用途 | 常见错误 |
|---|---|---|
jest.fn() |
独立的 Fake 回调或注入函数 | 在只需简单回调时,却用它替换了整个模块 |
jest.spyOn() |
观察或覆盖真实对象/模块上的单个方法 | 忘记恢复原始实现 |
jest.mock() |
在导入边界替换模块依赖 | 默认 Mock 大型模块,导致丢失有意义的行为 |
示例可以帮助:
- 处理当组件接收到
jest.fn()当组件接收到onSubmitprop 时。 - 使用
jest.spyOn()当需要验证console.error、存储方法或导出的 API 调用时。 - 使用
jest.mock()当导入模块会触发 I/O、原生 code 或超出单元测试边界的行为时。
许多指南都忽略了一个进阶领域:现代 React 中的错误路径测试。错误边界 (Error boundaries)、延迟的状态变更以及异步回退 UI 都需要编写高质量的测试,而不仅仅是测试“快乐路径”下的点击示例。如果子组件抛出错误,应断言回退 UI 是否正确显示;如果请求失败,应断言可见的恢复状态;如果按钮在加载期间被禁用,也应进行断言。这些才是用户感触最深的 Bug。
提升测试质量与策略
很多团队仍盲目追求测试覆盖率,认为它等同于测试信心。其实不然。
即使达到了覆盖率目标,你仍可能错过关键的回归问题。充斥着浅层断言、宽泛的 snapshot 以及对内部实现的 mock,不仅会营造出一种安全的假象,还会增加维护成本。

覆盖率是地图,而非终点
只有当覆盖率报告能回答一个问题时才有用:哪些关键路径目前还没有保护?
如果报告只是为了让开发者去测试琐碎的 wrapper、静态 markup 或只有一行的透传文件,以此来提升百分比,那它就毫无意义。应将覆盖率视为一种发现工具。如果身份验证状态、计费操作、feature flags 或 update prompts 缺乏测试,这就是一个信号;但如果只是一个展示用的 icon 组件没有测试,通常并不重要。
一个有效的评审问题很简单:这项测试是否降低了发布风险?
- 是: 它验证了关键路径上用户可见的行为。
- 可能: 它保护了在重构时容易出错的业务逻辑。
- 否: 它在断言实现细节,或者重复了其他测试的价值。
哪些内容不该进行单元测试
许多 React 指南仍未充分讨论“哪些不该测”。这个缺口至关重要,因为过度 mock 和测试实现细节会导致测试套件变得脆弱——即便测试通过,用户体验仍可能崩溃。正如 BrowserStack 在其关于 React 中哪些内容不该进行单元测试.
请跳过或严格限制以下模式:
- 内部状态断言: 不要测试
isOpen直接测试,而是测试面板是否已打开。 - 框架行为: 不要测试 React 是否调用了某个 effect。应该测试该 effect 所引起的结果变化。
- 第三方库的内部实现: 测试你与日期选择器或路由器的集成情况,而不是测试库本身的渲染逻辑。
- 过度拆分的单元: 如果你 mock 了所有的子组件和辅助函数,你可能已经不再是在测试有意义的行为了。
糟糕的测试比缺失测试更可怕,因为它们会阻碍重构,却依然无法发现生产环境的 bug。
一个实用的启发式方法是“边界所有权”。测试 code 所拥有的内容。除非你的集成层改变了契约,否则不要去测试 React、浏览器或成熟库已经实现的功能。
Snapshot 测试的利与弊
Snapshot 并非毫无用处,只是很容易被误用。
仅在输出稳定、简单且结构差异具有实际意义的组件中少量使用。避免在交互式或高度动态的组件中使用,因为它们会变成噪音。开发者会停止阅读它们,转而开始机械式地更新它们。
通常会有更好的替代方案:
- 对于条件渲染,断言关键文本是否存在。
- 对于视觉状态变化,断言相关的 role、label 或 attribute。
- 对于错误和回退逻辑,断言实际的消息内容或告警区域。
如果你的团队需要单元测试之外更广泛的质量保障流程,一个可靠的补充是 应用质量保证 (QA) 工作流 将测试、发布检查和回滚计划视为一个统一的系统。这种思维转变是提升测试质量最快的方式。不要再纠结于你写了多少测试,而要开始思考:哪些故障仍有可能触达用户。
将测试集成到跨平台 CI/CD Pipeline 中
仅在开发人员笔记本电脑上运行的测试套件只是个建议,而非有效的控制手段。
当每一次 pull request 都能在干净的环境中运行相同的检查,并在检查失败时拦截合并时,测试套件才真正具备了工程化能力。这听起来显而易见,但许多团队仍存在关键漏洞:测试手动运行、覆盖率报告变成可选、打包和发布任务在测试任务完成前就开始执行。这正是微小的 UI 回归演变成重大发布故障的原因。

每一次 pull request 都应触发相同的质量门禁
若要让 React 单元测试发挥安全网的作用,CI 需要具备以下核心要素:
- 在每次 pull request 时运行
- 根据 lockfile 安装依赖
- 始终使用相同的测试命令
- 测试失败时快速报错 (Fail fast)
- 仅在测试通过后发布 artifacts
这是 应用团队持续部署实践的核心。要在发布前建立信心,而不是发布后。
对于许多团队来说,一个简单的 GitHub Actions 工作流就足够了:
name: test
on:
pull_request:
push:
branches:
- main
jobs:
react-tests:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:ci
这并不花哨,而这正是重点。最强大的 pipeline 通常是最不容易出错的。
为什么这对 Capacitor 和 Electron 尤为重要
跨平台 React 应用的发布风险比纯浏览器应用更高,因为相同的 UI code 通常运行在具有不同运行时假设的不同容器中。
以下几个例子展示了 pipeline 的作用:
- Capacitor 应用: Web code 在本地可能通过测试,但当插件桥接、离线状态或应用生命周期等边缘情况在打包后改变行为时,可能会失败。
- Electron 应用: 渲染器组件可能依赖于 preload API、window 消息传递或仅限桌面端的 state,除非刻意进行 mock,否则在纯浏览器测试中这些都不存在。
- 共享发布流水线: 如果部署流程缺乏严格的发布门禁,一个错误的 bundle 可能会影响多个目标平台。
因此,单元测试应在打包任务前运行,而打包任务应在分发任务前运行。每个阶段都在降低风险。单元测试能快速捕获本地回归问题;平台打包用于验证环境假设;而人工审批或分阶段发布则负责最后的发布信心保障。
一个实用的 GitHub Actions 工作流
更成熟的 pipeline 通常会进行职责分离:
- 测试任务: 快速的单元测试与 hook 测试
- 构建任务: 仅在测试通过后进行生产构建
- 打包任务: Capacitor sync、Electron 打包或 artifact 打包
- 发布任务: 仅允许从已批准的分支或标签发布
对于向 Capacitor 或 Electron 应用推送 Live Update 的团队来说,发布工具链至关重要。该工作流中的一个选项是 Capgo,它能为 CapacitorJS 和 Electron 应用发布经过签名的 web bundles,并支持回滚和基于 channel 的发布控制。在实际操作中,这意味着你的 React 测试任务可以在 web bundle 晋升到 staging 或 production 环境之前,充当第一道硬性关卡。
运维原则非常简单:不要让发布基础设施去弥补测试的不足。只有在可靠的测试已经过滤掉错误变更后,才使用发布基础设施。
一套可靠的测试系统会改变团队的行为。工程师合并代码时不再犹豫;审核人员可以专注于边缘案例,而不是手动重复基础测试;发布经理也不再把每次部署都当成一场豪赌。这就是做好 React 单元测试带来的成果。
如果你的团队通过 Capacitor 或 Electron 发布 React 应用,那么发布安全性不仅仅取决于本地测试是否通过。 Capgo 为团队提供了一种受控的方式来发布签名的 web updates、指定发布 channel,并在无需等待应用商店审核的情况下回滚错误的 bundles,这与要求在部署前必须通过单元测试的 CI pipeline 完美契合。