跳到主要内容

测试

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

在本指南中,我们将介绍各种自动化方法,以确保您的应用程序按预期运行,包括从静态分析到端到端测试。

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

为什么要测试

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

测试的价值可能超出您的想象。修复代码中 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,您的组件还处理诸如 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 类组件特别容易测试其实现细节,例如内部状态、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 测试库允许您在应用程序屏幕中查找和控制元素:例如,您可以像真实用户一样实际地点击按钮或在 TextInputs 中插入文本。然后,您可以断言应用程序屏幕中是否存在某个元素、它是否可见、它包含什么文本等等。

E2E 测试为您提供了应用程序某部分正常工作的最高信心。权衡包括

  • 与所有其他类型的测试相比,编写它们更耗时
  • 它们运行速度较慢
  • 它们更容易出现不稳定性(“不稳定”测试是指在不更改代码的情况下随机通过和失败的测试)

尝试使用 E2E 测试覆盖您应用程序的关键部分:身份验证流程、核心功能、支付等。对于您应用程序的非关键部分,请使用更快的 JS 测试。您添加的测试越多,您的信心就越高,但您维护和运行它们的时间也越多。考虑权衡并决定最适合您的方案。

有几种 E2E 测试工具可用:在 React Native 社区中,Detox 是一个流行的框架,因为它专为 React Native 应用程序量身定制。在 iOS 和 Android 应用程序领域,另一个流行的库是 AppiumMaestro

总结

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


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