测试
随着代码库的扩展,意想不到的小错误和边缘情况可能会演变为更大的故障。Bug 会导致糟糕的用户体验,最终造成业务损失。防止脆弱编程的一种方法是在发布代码之前对其进行测试。
本指南将介绍确保您的应用按预期工作的各种自动化方法,从静态分析到端到端测试。
为什么需要测试
我们是人类,人类会犯错。测试很重要,因为它可以帮助您发现这些错误并验证您的代码是否正常工作。或许更重要的是,测试可以确保您的代码在未来添加新功能、重构现有功能或升级项目的主要依赖项时仍能继续工作。
测试的价值可能超出您的想象。修复代码中 bug 的最佳方法之一是编写一个能暴露 bug 的失败测试。然后,当您修复 bug 并重新运行测试时,如果测试通过,则表示 bug 已修复,并且永远不会再次引入代码库。
测试还可以作为新加入团队成员的文档。对于从未见过代码库的人来说,阅读测试可以帮助他们了解现有代码的工作原理。
最后但同样重要的是,更多的自动化测试意味着更少的手动 QA 时间,从而节省了宝贵的时间。
静态分析
提高代码质量的第一步是开始使用静态分析工具。静态分析会在您编写代码时检查错误,但无需运行任何代码。
- 代码检查器会分析代码以捕获常见错误,例如未使用代码,并帮助避免陷阱,标记样式指南中的“禁止”项,例如使用制表符而不是空格(反之亦然,取决于您的配置)。
- 类型检查可确保您传递给函数的构造与函数旨在接受的类型匹配,例如,防止将字符串传递给需要数字的计数函数。
React Native 开箱即用配置了两个此类工具:用于代码检查的 ESLint 和用于类型检查的 TypeScript。
编写可测试的代码
要开始测试,您首先需要编写可测试的代码。以飞机制造过程为例——在任何型号首次起飞以展示其所有复杂系统协同工作之前,单个部件都要经过测试,以确保其安全且功能正常。例如,机翼在极端负载下进行弯曲测试;发动机部件进行耐久性测试;挡风玻璃进行模拟鸟撞测试。
软件也是如此。您不是将整个程序写在一个包含许多代码行的大文件中,而是将代码写入多个小模块中,这样可以比测试组装好的整体更彻底地进行测试。因此,编写可测试的代码与编写清晰、模块化的代码是相互关联的。
为了使您的应用更易于测试,首先将应用中的视图部分(您的 React 组件)与业务逻辑和应用状态分离(无论您使用 Redux、MobX 还是其他解决方案)。通过这种方式,您可以保持业务逻辑测试(不应依赖您的 React 组件)独立于组件本身,组件本身的主要工作是渲染您的应用的 UI!
理论上,您可以将所有逻辑和数据获取都移出组件。这样,您的组件将完全专注于渲染。您的状态将完全独立于您的组件。您的应用逻辑将完全不需要任何 React 组件即可工作!
我们鼓励您在其他学习资源中进一步探索可测试代码的主题。
编写测试
编写了可测试的代码后,是时候编写一些实际的测试了!React Native 的默认模板附带了 Jest 测试框架。它包含一个针对此环境量身定制的预设,因此您可以立即高效地工作,而无需调整配置和模拟——稍后将详细介绍模拟。您可以使用 Jest 编写本指南中介绍的所有类型的测试。
如果您进行测试驱动开发,您实际上是先编写测试!这样,您的代码的可测试性就有了保证。
测试结构
您的测试应该简短,理想情况下只测试一件事。让我们从一个用 Jest 编写的单元测试示例开始
it('given a date in the past, colorForDueDate() returns red', () => {
expect(colorForDueDate('2000-10-20')).toBe('red');
});
测试由传递给 it
函数的字符串描述。请仔细编写描述,使其清晰地说明正在测试的内容。尽力涵盖以下内容
- 给定 - 某些前置条件
- 当 - 您正在测试的函数执行某些操作时
- 则 - 预期的结果
这也被称为 AAA(Arrange, Act, Assert,即准备、执行、断言)。
Jest 提供了 describe
函数来帮助组织您的测试。使用 describe
将属于同一功能的所有测试分组在一起。如果需要,describe
可以嵌套。您通常还会使用的其他函数是 beforeEach
或 beforeAll
,您可以使用它们来设置正在测试的对象。在 Jest api 参考中阅读更多内容。
如果您的测试有很多步骤或很多预期,您可能希望将其拆分为多个较小的测试。另外,请确保您的测试彼此完全独立。您的测试套件中的每个测试都必须能够独立执行,而无需先运行其他测试。反之,如果您将所有测试一起运行,第一个测试不得影响第二个测试的输出。
最后,作为开发人员,我们喜欢我们的代码运行良好且不会崩溃。对于测试来说,情况通常恰恰相反。将失败的测试视为一件好事!当测试失败时,通常意味着有些地方不对劲。这给了您在问题影响用户之前解决问题的机会。
单元测试
单元测试涵盖代码的最小部分,例如单独的函数或类。
当被测试对象有任何依赖项时,通常需要模拟它们,如下一段所述。
单元测试的一大优点是它们编写和运行速度快。因此,在工作时,您可以快速获得测试是否通过的反馈。Jest 甚至有一个选项可以持续运行与您正在编辑的代码相关的测试:监视模式。
模拟
有时,当您测试的对象具有外部依赖项时,您会想要“模拟”它们。“模拟”是指您用自己的实现替换代码的某些依赖项。
通常,在测试中使用真实对象比使用模拟对象更好,但在某些情况下这是不可能的。例如:当您的 JS 单元测试依赖于用 Java 或 Objective-C 编写的原生模块时。
想象一下您正在编写一个显示您所在城市当前天气的应用程序,并且您正在使用一些外部服务或其他提供天气信息的依赖项。如果服务告诉您正在下雨,您希望显示一张带有雨云的图像。您不希望在测试中调用该服务,因为
- 它可能会使测试变慢且不稳定(因为涉及网络请求)
- 每次运行测试时,服务可能会返回不同的数据
- 当您真正需要运行测试时,第三方服务可能会下线!
因此,您可以提供服务的模拟实现,有效地替换了数千行代码和一些联网温度计!
Jest 附带了从函数级别到模块级别模拟的模拟支持。
集成测试
在编写大型软件系统时,它的各个部分需要相互交互。在单元测试中,如果您的单元依赖于另一个单元,您有时会模拟该依赖项,用一个假的依赖项替换它。
在集成测试中,真实的单个单元被组合在一起(与您的应用程序中相同)并一起进行测试,以确保它们的协作按预期工作。这并不是说这里不会发生模拟:您仍然需要模拟(例如,模拟与天气服务的通信),但您需要的模拟比单元测试少得多。
请注意,关于集成测试含义的术语并不总是一致的。此外,单元测试和集成测试之间的界限可能并不总是清晰的。对于本指南,如果您的测试符合以下条件,则属于“集成测试”:
- 如上所述,组合了您的应用程序的多个模块
- 使用外部系统
- 向其他应用程序(例如天气服务 API)发出网络调用
- 进行任何类型的文件或数据库 I/O
组件测试
React 组件负责渲染您的应用,用户将直接与其输出交互。即使您的应用的业务逻辑具有较高的测试覆盖率且正确无误,如果没有组件测试,您仍然可能会向用户提供损坏的 UI。组件测试可能属于单元测试和集成测试,但由于它们是 React Native 的核心部分,我们将单独讨论它们。
对于测试 React 组件,您可能需要测试两件事
- 交互:确保组件在用户交互时行为正确(例如,当用户按下按钮时)
- 渲染:确保 React 使用的组件渲染输出正确(例如,按钮的外观和在 UI 中的位置)
例如,如果您有一个带有 onPress
监听器的按钮,您需要测试该按钮是否正确显示,以及点击按钮是否由组件正确处理。
有几个库可以帮助您测试这些:
- React Native Testing Library 在 React 的测试渲染器之上构建,并添加了下一段中描述的
fireEvent
和query
API。 - [已弃用] React 的 测试渲染器,与其核心同时开发,提供了一个 React 渲染器,可用于将 React 组件渲染为纯 JavaScript 对象,而不依赖于 DOM 或原生移动环境。
组件测试只是在 Node.js 环境中运行的 JavaScript 测试。它们不考虑任何支持 React Native 组件的 iOS、Android 或其他平台代码。因此,它们无法给您 100% 的信心,确保一切都为用户正常工作。如果 iOS 或 Android 代码中存在错误,它们将无法找到。
测试用户交互
除了渲染一些 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 之类的实现细节——虽然此类测试有效,但它们并非面向用户将如何与组件交互,并且在重构时容易中断(例如,当您想要重命名某些内容或使用 hooks 重写类组件时)。
React 类组件尤其容易测试其实现细节,例如内部状态、props 或事件处理程序。为了避免测试实现细节,最好使用带有 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 的字符串。这个序列化器允许 Jest 将 React 组件树转换为人类可读的字符串。换句话说:组件快照是组件渲染输出在测试运行期间生成的文本表示。它可能看起来像这样
<Text
style={
Object {
"fontSize": 20,
"textAlign": "center",
}
}>
Welcome to React Native!
</Text>
使用快照测试时,通常是先实现组件,然后运行快照测试。然后,快照测试会创建快照并将其保存到存储库中的文件中作为参考快照。该文件随后会被提交并在代码审查期间进行检查。未来对组件渲染输出的任何更改都会更改其快照,这将导致测试失败。然后,您需要更新存储的参考快照以使测试通过。该更改再次需要提交和审查。
快照有几个弱点
- 对于作为开发人员或审阅者的您来说,很难判断快照中的更改是预期的还是错误的证据。特别是大型快照会很快变得难以理解,其附加值也会降低。
- 当创建快照时,此时它被认为是正确的——即使在渲染输出实际上是错误的情况下也是如此。
- 当快照失败时,很容易不加思索地使用
--updateSnapshot
jest 选项更新它,而没有仔细调查更改是否是预期的。因此,需要一定的开发人员纪律。
快照本身并不能确保您的组件渲染逻辑是正确的,它们仅仅擅长防止意外更改以及检查被测试的 React 树中的组件是否接收到预期的 props(样式等)。
我们建议您只使用小快照(参见 no-large-snapshots
规则)。如果您想测试两个 React 组件状态之间的变化,请使用 snapshot-diff
。如有疑问,请优先使用上一段中描述的明确预期。
端到端测试
在端到端 (E2E) 测试中,您会从用户的角度验证您的应用程序在设备(或模拟器/仿真器)上是否按预期工作。
这是通过在发布配置中构建您的应用程序并对其运行测试来完成的。在 E2E 测试中,您不再考虑 React 组件、React Native API、Redux 存储或任何业务逻辑。这不是 E2E 测试的目的,并且在 E2E 测试期间甚至无法访问这些。
相反,E2E 测试库允许您查找和控制应用程序屏幕中的元素:例如,您可以像真实用户一样实际点击按钮或将文本插入到 TextInput
中。然后,您可以断言应用程序屏幕中是否存在某个元素、它是否可见、它包含什么文本等等。
E2E 测试为您提供了应用程序部分正在工作的最高置信度。权衡包括
- 与其它类型的测试相比,编写它们更耗时
- 它们运行速度较慢
- 它们更容易出现“不稳定”(“不稳定”测试是指在代码没有任何变化的情况下随机通过和失败的测试)
尝试使用 E2E 测试覆盖您应用程序的关键部分:身份验证流程、核心功能、支付等。对于应用程序的非关键部分,请使用更快的 JS 测试。您添加的测试越多,您的信心就越高,但同时,您维护和运行它们所花费的时间也越多。权衡利弊,并决定最适合您的方案。
有几种可用的 E2E 测试工具:在 React Native 社区中,Detox 是一个流行的框架,因为它专为 React Native 应用程序量身定制。在 iOS 和 Android 应用程序领域,另一个流行的库是 Appium 或 Maestro。
总结
我们希望您喜欢阅读并从本指南中学到了一些东西。测试应用程序的方法有很多种。一开始可能很难决定使用哪种。但是,我们相信一旦您开始向您出色的 React Native 应用程序添加测试,一切都将变得有意义。那么您还在等什么?提高您的覆盖率吧!
链接
本指南最初由 Vojtech Novak 撰写并完整贡献。