跳到主要内容

测试

随着代码库的扩展,意想不到的小错误和边缘情况可能会演变为更大的故障。Bug 会导致糟糕的用户体验,最终造成业务损失。防止脆弱编程的一种方法是在发布代码之前对其进行测试。

本指南将介绍确保您的应用按预期工作的各种自动化方法,从静态分析到端到端测试。

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

为什么需要测试

我们是人类,人类会犯错。测试很重要,因为它可以帮助您发现这些错误并验证您的代码是否正常工作。或许更重要的是,测试可以确保您的代码在未来添加新功能、重构现有功能或升级项目的主要依赖项时仍能继续工作。

测试的价值可能超出您的想象。修复代码中 bug 的最佳方法之一是编写一个能暴露 bug 的失败测试。然后,当您修复 bug 并重新运行测试时,如果测试通过,则表示 bug 已修复,并且永远不会再次引入代码库。

测试还可以作为新加入团队成员的文档。对于从未见过代码库的人来说,阅读测试可以帮助他们了解现有代码的工作原理。

最后但同样重要的是,更多的自动化测试意味着更少的手动 QA 时间,从而节省了宝贵的时间。

静态分析

提高代码质量的第一步是开始使用静态分析工具。静态分析会在您编写代码时检查错误,但无需运行任何代码。

  • 代码检查器会分析代码以捕获常见错误,例如未使用代码,并帮助避免陷阱,标记样式指南中的“禁止”项,例如使用制表符而不是空格(反之亦然,取决于您的配置)。
  • 类型检查可确保您传递给函数的构造与函数旨在接受的类型匹配,例如,防止将字符串传递给需要数字的计数函数。

React Native 开箱即用配置了两个此类工具:用于代码检查的 ESLint 和用于类型检查的 TypeScript

编写可测试的代码

要开始测试,您首先需要编写可测试的代码。以飞机制造过程为例——在任何型号首次起飞以展示其所有复杂系统协同工作之前,单个部件都要经过测试,以确保其安全且功能正常。例如,机翼在极端负载下进行弯曲测试;发动机部件进行耐久性测试;挡风玻璃进行模拟鸟撞测试。

软件也是如此。您不是将整个程序写在一个包含许多代码行的大文件中,而是将代码写入多个小模块中,这样可以比测试组装好的整体更彻底地进行测试。因此,编写可测试的代码与编写清晰、模块化的代码是相互关联的。

为了使您的应用更易于测试,首先将应用中的视图部分(您的 React 组件)与业务逻辑和应用状态分离(无论您使用 Redux、MobX 还是其他解决方案)。通过这种方式,您可以保持业务逻辑测试(不应依赖您的 React 组件)独立于组件本身,组件本身的主要工作是渲染您的应用的 UI!

理论上,您可以将所有逻辑和数据获取都移出组件。这样,您的组件将完全专注于渲染。您的状态将完全独立于您的组件。您的应用逻辑将完全不需要任何 React 组件即可工作!

我们鼓励您在其他学习资源中进一步探索可测试代码的主题。

编写测试

编写了可测试的代码后,是时候编写一些实际的测试了!React Native 的默认模板附带了 Jest 测试框架。它包含一个针对此环境量身定制的预设,因此您可以立即高效地工作,而无需调整配置和模拟——稍后将详细介绍模拟。您可以使用 Jest 编写本指南中介绍的所有类型的测试。

如果您进行测试驱动开发,您实际上是先编写测试!这样,您的代码的可测试性就有了保证。

测试结构

您的测试应该简短,理想情况下只测试一件事。让我们从一个用 Jest 编写的单元测试示例开始

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

测试由传递给 it 函数的字符串描述。请仔细编写描述,使其清晰地说明正在测试的内容。尽力涵盖以下内容

  1. 给定 - 某些前置条件
  2. - 您正在测试的函数执行某些操作时
  3. - 预期的结果

这也被称为 AAA(Arrange, Act, Assert,即准备、执行、断言)。

Jest 提供了 describe 函数来帮助组织您的测试。使用 describe 将属于同一功能的所有测试分组在一起。如果需要,describe 可以嵌套。您通常还会使用的其他函数是 beforeEachbeforeAll,您可以使用它们来设置正在测试的对象。在 Jest api 参考中阅读更多内容。

如果您的测试有很多步骤或很多预期,您可能希望将其拆分为多个较小的测试。另外,请确保您的测试彼此完全独立。您的测试套件中的每个测试都必须能够独立执行,而无需先运行其他测试。反之,如果您将所有测试一起运行,第一个测试不得影响第二个测试的输出。

最后,作为开发人员,我们喜欢我们的代码运行良好且不会崩溃。对于测试来说,情况通常恰恰相反。将失败的测试视为一件好事!当测试失败时,通常意味着有些地方不对劲。这给了您在问题影响用户之前解决问题的机会。

单元测试

单元测试涵盖代码的最小部分,例如单独的函数或类。

当被测试对象有任何依赖项时,通常需要模拟它们,如下一段所述。

单元测试的一大优点是它们编写和运行速度快。因此,在工作时,您可以快速获得测试是否通过的反馈。Jest 甚至有一个选项可以持续运行与您正在编辑的代码相关的测试:监视模式

模拟

有时,当您测试的对象具有外部依赖项时,您会想要“模拟”它们。“模拟”是指您用自己的实现替换代码的某些依赖项。

通常,在测试中使用真实对象比使用模拟对象更好,但在某些情况下这是不可能的。例如:当您的 JS 单元测试依赖于用 Java 或 Objective-C 编写的原生模块时。

想象一下您正在编写一个显示您所在城市当前天气的应用程序,并且您正在使用一些外部服务或其他提供天气信息的依赖项。如果服务告诉您正在下雨,您希望显示一张带有雨云的图像。您不希望在测试中调用该服务,因为

  • 它可能会使测试变慢且不稳定(因为涉及网络请求)
  • 每次运行测试时,服务可能会返回不同的数据
  • 当您真正需要运行测试时,第三方服务可能会下线!

因此,您可以提供服务的模拟实现,有效地替换了数千行代码和一些联网温度计!

Jest 附带了从函数级别到模块级别模拟的模拟支持

集成测试

在编写大型软件系统时,它的各个部分需要相互交互。在单元测试中,如果您的单元依赖于另一个单元,您有时会模拟该依赖项,用一个假的依赖项替换它。

在集成测试中,真实的单个单元被组合在一起(与您的应用程序中相同)并一起进行测试,以确保它们的协作按预期工作。这并不是说这里不会发生模拟:您仍然需要模拟(例如,模拟与天气服务的通信),但您需要的模拟比单元测试少得多。

请注意,关于集成测试含义的术语并不总是一致的。此外,单元测试和集成测试之间的界限可能并不总是清晰的。对于本指南,如果您的测试符合以下条件,则属于“集成测试”:

  • 如上所述,组合了您的应用程序的多个模块
  • 使用外部系统
  • 向其他应用程序(例如天气服务 API)发出网络调用
  • 进行任何类型的文件或数据库 I/O

组件测试

React 组件负责渲染您的应用,用户将直接与其输出交互。即使您的应用的业务逻辑具有较高的测试覆盖率且正确无误,如果没有组件测试,您仍然可能会向用户提供损坏的 UI。组件测试可能属于单元测试和集成测试,但由于它们是 React Native 的核心部分,我们将单独讨论它们。

对于测试 React 组件,您可能需要测试两件事

  • 交互:确保组件在用户交互时行为正确(例如,当用户按下按钮时)
  • 渲染:确保 React 使用的组件渲染输出正确(例如,按钮的外观和在 UI 中的位置)

例如,如果您有一个带有 onPress 监听器的按钮,您需要测试该按钮是否正确显示,以及点击按钮是否由组件正确处理。

有几个库可以帮助您测试这些:

  • React Native Testing Library 在 React 的测试渲染器之上构建,并添加了下一段中描述的 fireEventquery API。
  • [已弃用] React 的 测试渲染器,与其核心同时开发,提供了一个 React 渲染器,可用于将 React 组件渲染为纯 JavaScript 对象,而不依赖于 DOM 或原生移动环境。

组件测试只是在 Node.js 环境中运行的 JavaScript 测试。它们考虑任何支持 React Native 组件的 iOS、Android 或其他平台代码。因此,它们无法给您 100% 的信心,确保一切都为用户正常工作。如果 iOS 或 Android 代码中存在错误,它们将无法找到。

测试用户交互

除了渲染一些 UI,您的组件还处理诸如 `TextInput` 的 `onChangeText` 或 `Button` 的 `onPress` 之类的事件。它们还可能包含其他函数和事件回调。考虑以下示例

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 之类的实现细节——虽然此类测试有效,但它们并非面向用户将如何与组件交互,并且在重构时容易中断(例如,当您想要重命名某些内容或使用 hooks 重写类组件时)。

React 类组件尤其容易测试其实现细节,例如内部状态、props 或事件处理程序。为了避免测试实现细节,最好使用带有 Hooks 的函数组件,这使得依赖组件内部变得更难

诸如 React Native Testing Library 之类的组件测试库通过精心选择提供的 API 来促进以用户为中心的测试。以下示例使用 fireEvent 方法 changeTextpress 模拟用户与组件的交互,以及查询函数 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 的字符串。这个序列化器允许 Jest 将 React 组件树转换为人类可读的字符串。换句话说:组件快照是组件渲染输出在测试运行期间生成的文本表示。它可能看起来像这样

tsx
<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 应用程序领域,另一个流行的库是 AppiumMaestro

总结

我们希望您喜欢阅读并从本指南中学到了一些东西。测试应用程序的方法有很多种。一开始可能很难决定使用哪种。但是,我们相信一旦您开始向您出色的 React Native 应用程序添加测试,一切都将变得有意义。那么您还在等什么?提高您的覆盖率吧!


本指南最初由 Vojtech Novak 撰写并完整贡献。