跳至主要内容

测试

随着代码库的扩展,您意想不到的小错误和极端情况可能会级联成更大的故障。错误会导致糟糕的用户体验,并最终导致业务损失。防止脆弱编程的一种方法是在将代码发布到生产环境之前对其进行测试。

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

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

为什么要测试

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

测试的价值可能超乎您的想象。修复代码中错误的最佳方法之一是编写一个失败的测试来暴露它。然后,当您修复错误并重新运行测试时,如果它通过了,则意味着错误已修复,并且永远不会重新引入到代码库中。

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

最后但并非最不重要的一点是,更多的自动化测试意味着更少的时间花费在手动 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 函数的字符串描述。认真编写描述,以便清楚地说明正在测试的内容。尽力涵盖以下内容

  1. 给定 - 一些前提条件
  2. - 您正在测试的函数执行的一些操作
  3. 然后 - 预期的结果

这也被称为 AAA(安排、操作、断言)。

Jest 提供了 describe 函数来帮助您构建测试。使用 describe 将属于一个功能的所有测试组合在一起。如果需要,可以嵌套描述。您通常会使用的其他函数是 beforeEachbeforeAll,您可以使用它们来设置要测试的对象。在 Jest api 参考 中了解更多信息。

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

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

单元测试

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

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

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

模拟

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

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

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

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

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

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

集成测试

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

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

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

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

组件测试

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

对于测试 React 组件,您可能希望测试两件事

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

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

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

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

组件测试只是在 Node.js 环境中运行的 JavaScript 测试。它们不会考虑任何支持 React Native 组件的 iOS、Android 或其他平台代码。因此,它们无法让你 100% 确信一切对用户都有效。如果 iOS 或 Android 代码中存在错误,它们将无法发现。

测试用户交互

除了渲染一些 UI 外,你的组件还处理诸如 TextInputonChangeTextButtononPress 等事件。它们可能还包含其他函数和事件回调。请考虑以下示例

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 查询

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

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

诸如 React Native 测试库 等组件测试库通过仔细选择提供的 API 来促进编写以用户为中心的测试。以下示例使用 fireEvent 方法 changeTextpress 来模拟用户与组件的交互,以及一个查询函数 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 树中的组件是否接收了预期的属性(样式等)。

我们建议你只使用小型快照(请参阅 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 全面撰写和贡献。