测试概述
本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →
随着代码库规模扩大,未曾预料的小错误和边界情况可能引发连锁故障。程序缺陷会导致糟糕的用户体验,最终造成业务损失。防止这种隐患的方法之一就是在发布前对代码进行充分测试。
本指南将介绍多种自动化测试方法,涵盖从静态分析到端到端测试的全流程,确保你的应用行为符合预期。
为何需要测试
人非圣贤孰能无过。测试的重要性在于它能帮你发现潜在错误并验证代码功能。更重要的是,当你添加新功能、重构现有代码或升级项目依赖时,测试能确保系统持续稳定运行。
测试的价值远超预期。修复代码缺陷的最高效方式,就是编写一个能暴露该问题的失败测试用例。修复后重新运行测试,若通过则表明缺陷已彻底解决且不会重现。
测试用例还能为新团队成员提供活文档。初次接触代码库的开发者通过阅读测试,能快速理解现有代码的运行机制。
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 编写的单元测试示例:
it('given a date in the past, colorForDueDate() returns red', () => {
expect(colorForDueDate('2000-10-20')).toBe('red');
});
测试通过传递给 it 函数的字符串描述。请精心编写描述以明确测试目标,确保涵盖:
-
给定条件(Given)—— 前置状态
-
执行操作(When)—— 被测函数的具体行为
-
预期结果(Then)—— 期望的输出
这也称为 AAA 模式(准备 Arrange,执行 Act,断言 Assert)。
Jest 提供 describe 函数帮助组织测试。用 describe 将相同功能的测试分组,支持嵌套使用。常用函数还有 beforeEach 或 beforeAll,用于初始化测试对象。详见 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 测试渲染器构建,提供下文所述的
fireEvent和queryAPI -
[已弃用] React 官方的 Test Renderer 可将组件渲染为纯 JavaScript 对象,无需依赖 DOM 或原生环境
组件测试仅在Node.js环境中运行JavaScript代码,不涉及iOS/Android等平台的底层原生代码。因此无法100%保证用户端功能完整性——若原生代码存在缺陷,此类测试无法捕获。
用户交互测试
除渲染 UI 外,组件还需处理如 TextInput 的 onChangeText 或 Button 的 onPress 等事件,其中可能包含各类函数和回调。参考以下示例:
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,帮助开发者编写以用户为中心的测试。以下示例使用 fireEvent 的 changeText 和 press 方法模拟用户交互,并通过查询函数 getAllByText 在渲染输出中查找匹配的 Text 节点。
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 组件树转换为人类可读的字符串——换句话说,快照是测试运行期间生成的组件渲染输出的文本表示,可能如下所示:
<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 的流行方案包括 Appium 和 Maestro。
总结
希望本指南让你有所收获。应用测试方法多样,初期选择或显困难。但相信当你开始为 React Native 应用添加测试时,一切都会变得清晰。还在等什么?赶快提升测试覆盖率吧!
相关链接
本指南由 Vojtech Novak 原创并完整贡献。