跳到主要内容

测试

随着代码库的扩展,您未预料到的小错误和边缘情况可能会级联成更大的故障。Bug 会导致糟糕的用户体验,并最终导致业务损失。防止脆弱编程的一种方法是在将代码发布到实际应用之前对其进行测试。

在本指南中,我们将介绍不同的自动化方法,以确保您的应用程序按预期工作,范围从静态分析到端到端测试。

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

为何测试

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

测试的价值比您可能意识到的要多。修复代码中 bug 的最佳方法之一是编写一个暴露该 bug 的失败测试。然后,当您修复 bug 并重新运行测试时,如果测试通过,则意味着 bug 已被修复,永远不会重新引入到代码库中。

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

最后但并非最不重要的一点是,更多的自动化测试意味着更少的时间用于手动 QA,从而释放宝贵的时间。

静态分析

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

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

React Native 自带两个开箱即用的此类工具:ESLint 用于 linting,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 将属于一项功能的所有测试分组在一起。如果需要,可以嵌套描述。您常用的其他函数是 beforeEachbeforeAll,您可以用于设置正在测试的对象。在 Jest api 参考中阅读更多内容。

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

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

单元测试

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

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

单元测试的好处是它们编写和运行速度很快。因此,在您工作时,您可以快速获得有关测试是否通过的反馈。Jest 甚至可以选择持续运行与您正在编辑的代码相关的测试:Watch 模式

模拟

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

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

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

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

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

Jest 提供 对模拟的支持,从函数级别一直到模块级别模拟。

集成测试

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

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

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

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

组件测试

React 组件负责渲染您的应用程序,用户将直接与它们的输出进行交互。即使您的应用程序的业务逻辑具有高测试覆盖率并且是正确的,如果没有组件测试,您仍然可能向用户交付损坏的 UI。组件测试可以归入单元测试和集成测试,但由于它们是 React Native 的核心部分,我们将单独介绍它们。

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

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

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

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

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

组件测试只是在 Node.js 环境中运行的 JavaScript 测试。它们考虑任何支持 React Native 组件的 iOS、Android 或其他平台代码。因此,它们不能 100% 保证一切对用户都有效。如果 iOS 或 Android 代码中存在 bug,它们将无法找到。

测试用户交互

除了渲染一些 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>
))}
</>
);
}

在测试用户交互时,从用户的角度测试组件 - 页面上有什么?交互时会发生什么变化?

根据经验,最好使用用户可以看到或听到的东西

相反,您应该避免

  • 对组件属性或状态进行断言
  • testID 查询

避免测试实现细节,例如属性或状态 - 虽然此类测试有效,但它们不是面向用户如何与组件交互的,并且容易因重构而中断(例如,当您想重命名某些内容或使用 Hook 重写类组件时)。

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

组件测试库(例如 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>

通过快照测试,您通常首先实现您的组件,然后运行快照测试。然后,快照测试会创建一个快照,并将其作为参考快照保存到您仓库中的文件中。然后提交该文件并在代码审查期间进行检查。未来对组件渲染输出的任何更改都将更改其快照,这将导致测试失败。然后,您需要更新存储的参考快照才能使测试通过。该更改再次需要提交和审查。

快照有几个弱点

  • 作为开发人员或审查者,您可能很难判断快照中的更改是预期的,还是 bug 的证据。特别是大型快照很快就会变得难以理解,并且它们的附加值会变得很低。
  • 当创建快照时,此时它被认为是正确的 - 即使在渲染输出实际上是错误的情况下也是如此。
  • 当快照失败时,很容易使用 --updateSnapshot jest 选项更新它,而没有适当地注意调查更改是否是预期的。因此,需要一定的开发人员纪律。

快照本身并不能确保您的组件渲染逻辑是正确的,它们只是擅长防止意外更改,并检查被测 React 树中的组件是否接收到预期的属性(样式等)。

我们建议您仅使用小型快照(请参阅 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 撰写并完整贡献。