跳至主内容

测试概述

非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

随着代码库规模扩大,未曾预料的小错误和边界情况可能引发连锁故障。程序缺陷会导致糟糕的用户体验,最终造成业务损失。防止这种隐患的方法之一就是在发布前对代码进行充分测试。

本指南将介绍多种自动化测试方法,涵盖从静态分析到端到端测试的全流程,确保你的应用行为符合预期。

Testing is a cycle of fixing, testing, and either passing to release or failing back into testing.

为何需要测试

人非圣贤孰能无过。测试的重要性在于它能帮你发现潜在错误并验证代码功能。更重要的是,当你添加新功能、重构现有代码或升级项目依赖时,测试能确保系统持续稳定运行。

测试的价值远超预期。修复代码缺陷的最高效方式,就是编写一个能暴露该问题的失败测试用例。修复后重新运行测试,若通过则表明缺陷已彻底解决且不会重现。

测试用例还能为新团队成员提供活文档。初次接触代码库的开发者通过阅读测试,能快速理解现有代码的运行机制。

Last but not least, more automated testing means less time spent with manual QA, freeing up valuable time.

静态分析

提升代码质量的第一步是采用静态分析工具。这类工具能在不运行代码的前提下,实时检测编写过程中的错误。

  • Linters 通过分析代码捕捉常见问题(如未使用代码),规避潜在陷阱,并标记违反风格指南的行为(例如配置要求使用空格时误用制表符)。

  • 类型检查 确保传入函数的参数符合定义,例如防止向需要数字参数的计数函数传递字符串。

React Native 内置了两大工具:ESLint 负责代码规范检查,TypeScript 提供类型校验功能。

编写可测试的代码

编写可测试代码是测试的前提。以飞机制造为例:在整机试飞验证复杂系统协作前,每个部件都需独立测试。机翼需进行极限负载弯曲测试,发动机零件需验证耐用性,挡风玻璃需通过模拟鸟撞试验。

软件开发同理。相较于将整个程序塞进单一巨型文件,将代码拆分为小型模块能进行更彻底的测试。因此,编写可测试代码与构建清晰、模块化的代码结构密不可分。

提升应用可测试性的关键在于:将视图层(React 组件)与业务逻辑及应用状态(无论使用 Redux、MobX 或其他方案)解耦。这样既能独立测试不依赖组件的业务逻辑,又能让组件专注核心职责——渲染用户界面。

理想情况下,应将所有逻辑和数据获取移出组件。此时组件仅负责渲染,状态完全独立于组件存在,应用逻辑甚至可在脱离 React 组件的环境下运行!

技巧

建议您通过其他学习资源深入探索可测试代码的编写技巧。

编写测试

编写完可测试代码后,是时候编写实际测试了!React Native 的默认模板内置了 Jest 测试框架。它包含针对此环境预设的配置,无需立即调整配置和模拟(稍后会详细介绍模拟)。你可以使用 Jest 编写本指南涉及的所有测试类型。

备注

如果你采用测试驱动开发(TDD),实际上你会先编写测试!这样就能确保代码天然具备可测试性。

组织测试结构

测试应保持简短,且最好只验证单一功能。以下是用 Jest 编写的单元测试示例:

js
it('given a date in the past, colorForDueDate() returns red', () => {
expect(colorForDueDate('2000-10-20')).toBe('red');
});

测试通过传递给 it 函数的字符串描述。请精心编写描述以明确测试目标,确保涵盖:

  1. 给定条件(Given)—— 前置状态

  2. 执行操作(When)—— 被测函数的具体行为

  3. 预期结果(Then)—— 期望的输出

这也称为 AAA 模式(准备 Arrange,执行 Act,断言 Assert)。

Jest 提供 describe 函数帮助组织测试。用 describe 将相同功能的测试分组,支持嵌套使用。常用函数还有 beforeEachbeforeAll,用于初始化测试对象。详见 Jest API 文档

若测试步骤或断言过多,建议拆分为多个小测试。确保测试完全独立:每个测试必须能单独运行,且测试套件中任一测试的执行结果不影响其他测试。

开发者通常期望代码完美运行,但测试场景恰恰相反——请将测试失败视为_好事_!失败往往意味着存在问题,这使你有机会在影响用户前修复它。

单元测试

单元测试覆盖代码最小单元,如独立函数或类。

当被测对象存在依赖项时,通常需要模拟(mock)它们,详见下文。

单元测试的优势在于编写和运行速度快,能即时反馈测试结果。Jest 还提供 Watch 模式,可实时运行与编辑代码相关的测试。

模拟(Mocking)

当被测对象依赖外部模块时,常需通过“模拟”将其替换为自定义实现。

信息

通常测试中使用真实对象优于模拟,但某些场景不可行。例如:JS 单元测试依赖 Java/Objective-C 编写的原生模块时。

假设你开发的城市天气应用依赖外部天气服务。若服务返回“下雨”,需显示雨云图标。测试中不应直接调用该服务,因为:

  • 网络请求会导致测试变慢且不稳定

  • 每次运行测试可能返回不同数据

  • 第三方服务可能会在你急需运行测试时掉线!

因此,你可以提供该服务的模拟实现(mock),从而有效替代数千行代码和那些联网的温度计!

备注

Jest 内置了完善的模拟功能支持,从函数级别到模块级别的模拟均可实现。

集成测试

在构建大型软件系统时,其各个组件需要相互协作。在单元测试中,如果某个单元依赖其他组件,你最终可能需要通过模拟(mock)来创建虚假依赖。

集成测试会将真实的独立单元(如同在生产应用中)组合起来进行联合测试,确保它们的协同工作符合预期。这并不意味着完全不需要模拟:你仍可能需要模拟某些依赖(例如模拟天气服务的通信),但所需模拟量远少于单元测试。

信息

请注意"集成测试"的术语定义存在差异。单元测试与集成测试的界限有时并不明确。本指南采用以下判定标准:

  • 组合多个应用模块(如上所述)
  • 使用外部系统
  • 发起网络请求(如天气服务API)
  • 执行文件或数据库I/O操作

组件测试

React 组件负责渲染应用界面,用户将直接与其输出交互。即使应用的业务逻辑测试覆盖完备且正确,缺乏组件测试仍可能导致向用户交付损坏的 UI。组件测试可归为单元测试或集成测试,但由于其在 React Native 中的核心地位,我们将单独讨论。

测试 React 组件时,通常关注两个方面:

  • 交互行为:确保用户操作时组件响应正确(例如用户点击按钮)

  • 渲染输出:确保 React 使用的渲染结果准确(例如按钮的视觉呈现和布局位置)

例如,若某个按钮设置了 onPress 监听器,你既要测试其视觉呈现正确,也要验证点击事件能被组件正确处理。

推荐使用以下测试库:

  • React Native Testing Library 基于 React 测试渲染器构建,提供下文所述的 fireEventquery API

  • [已弃用] React 官方的 Test Renderer 可将组件渲染为纯 JavaScript 对象,无需依赖 DOM 或原生环境

警告

组件测试仅在Node.js环境中运行JavaScript代码,不涉及iOS/Android等平台的底层原生代码。因此无法100%保证用户端功能完整性——若原生代码存在缺陷,此类测试无法捕获。

用户交互测试

除渲染 UI 外,组件还需处理如 TextInputonChangeTextButtononPress 等事件,其中可能包含各类函数和回调。参考以下示例:

tsx
function GroceryShoppingList() {
const [groceryItem, setGroceryItem] = useState('');
const [items, setItems] = useState<string[]>([]);

const addNewItemToShoppingList = useCallback(() => {
setItems([groceryItem, ...items]);
setGroceryItem('');
}, [groceryItem, items]);

return (
<>
<TextInput
value={groceryItem}
placeholder="Enter grocery item"
onChangeText={text => setGroceryItem(text)}
/>
<Button
title="Add the item to list"
onPress={addNewItemToShoppingList}
/>
{items.map(item => (
<Text key={item}>{item}</Text>
))}
</>
);
}

测试用户交互时,应从用户视角出发:界面呈现什么?交互后发生什么变化?

基本原则:优先使用用户可感知的元素进行验证:

反之应避免:

  • 对组件的 props 或 state 进行断言

  • testID 查询

避免测试实现细节,例如 props 或 state——虽然这类测试可行,但它们并不以用户如何与组件交互为导向,并且在重构时容易失效(例如,当你想要重命名某些内容或使用 Hook 重写类组件时)。

信息

React 类组件尤其容易暴露实现细节(如内部状态、属性或事件处理程序)的测试。为避免测试实现细节,建议使用带有 Hooks 的函数组件,这使得依赖组件内部细节变得更加困难。

React Native Testing Library 等组件测试库通过精心设计的 API,帮助开发者编写以用户为中心的测试。以下示例使用 fireEventchangeTextpress 方法模拟用户交互,并通过查询函数 getAllByText 在渲染输出中查找匹配的 Text 节点。

tsx
test('given empty GroceryShoppingList, user can add an item to it', () => {
const {getByPlaceholderText, getByText, getAllByText} = render(
<GroceryShoppingList />,
);

fireEvent.changeText(
getByPlaceholderText('Enter grocery item'),
'banana',
);
fireEvent.press(getByText('Add the item to list'));

const bananaElements = getAllByText('banana');
expect(bananaElements).toHaveLength(1); // expect 'banana' to be on the list
});

这个示例并非测试调用函数时状态如何变化,而是测试当用户在 TextInput 中修改文本并点击 Button 时会发生什么!

测试渲染输出

快照测试是 Jest 提供的高级测试方法。作为强大但底层的工具,使用时需要格外谨慎。

"组件快照"是由 Jest 内置的自定义 React 序列化器生成的类 JSX 字符串。该序列化器将 React 组件树转换为人类可读的字符串——换句话说,快照是测试运行期间生成的组件渲染输出的文本表示,可能如下所示:

tsx
<Text
style={
Object {
"fontSize": 20,
"textAlign": "center",
}
}>
Welcome to React Native!
</Text>

快照测试通常先实现组件再运行测试,生成快照后将其作为参考快照保存在仓库文件中。该文件需提交并通过代码审查。后续任何渲染输出的改动都会导致快照变化,使测试失败。此时需更新参考快照并通过提交和审查流程。

快照测试存在几个弱点:

  • 开发者或审查者难以判断快照变化是预期改动还是错误证据,大型快照尤其难以理解且价值有限

  • 快照创建时即被视为正确,即使渲染输出实际存在错误

  • 快照失败时,开发者可能未经充分审查就使用 --updateSnapshot 选项更新快照,这需要严格的开发纪律

快照本身不能确保组件渲染逻辑正确,其主要价值在于防止意外变更,并验证被测 React 树中的组件是否接收到预期 props(如样式等)。

我们建议仅使用小型快照(参见 no-large-snapshots 规则)。若需测试 React 组件两种状态间的差异,推荐使用 snapshot-diff。不确定时,优先采用前文所述的显式断言方法。

端到端测试

端到端(E2E)测试从用户视角验证应用在设备(或模拟器/仿真器)上的实际表现。

这需要以发布模式构建应用并运行测试。E2E 测试不再关注 React 组件、React Native API、Redux store 或业务逻辑——这些既非 E2E 测试目的,在测试过程中也无法访问。

相反,端到端(E2E)测试库允许你在应用界面中查找并操控元素:例如,你可以像真实用户那样_实际_点击按钮或在 TextInputs 中输入文字。随后你可以断言特定元素是否存在于应用界面中、是否可见、包含什么文本内容等。

端到端测试能为你提供最高级别的信心保障,但代价包括:

  • 编写耗时比其他测试类型更长

  • 运行速度较慢

  • 更容易出现不稳定性("不稳定测试"指随机通过或失败,且代码未作任何更改)

建议对应用的关键路径进行端到端测试覆盖:认证流程、核心功能、支付模块等。非关键路径可使用更快的 JavaScript 测试。测试越多信心越足,但维护和执行时间也会增加。请根据实际需求权衡利弊。

现有多种端到端测试工具可选:在 React Native 社区中,Detox 因其专为 React Native 设计而广受欢迎;其他适用于 iOS/Android 的流行方案包括 AppiumMaestro

总结

希望本指南让你有所收获。应用测试方法多样,初期选择或显困难。但相信当你开始为 React Native 应用添加测试时,一切都会变得清晰。还在等什么?赶快提升测试覆盖率吧!

相关链接


本指南由 Vojtech Novak 原创并完整贡献。