现代 React 测试,第一部分:最佳实践
如果您喜欢这篇文章,请订阅我的时事通讯。
本系列文章深入剖析了 React 组件和前端测试的现状,不仅解释了“如何”的问题,更解释了“为什么”。我们将讨论为什么要编写自动化测试、编写哪些测试以及如何编写。在实践文章中,我们将学习如何使用 Jest、Enzyme 和 React Testing Library 来测试 React 组件。
三年前我写过一篇类似的文章,现在我把它看作一本不良行为手册。我当时推荐的几乎所有做法,现在几乎都不做了。
这是系列文章的第一篇,我们将在其中了解测试自动化为何有用、要编写哪些类型的测试以及测试最佳实践。
- 现代 React 测试:最佳实践(本文)
- 现代 React 测试:Jest 和 Enzyme
- 现代 React 测试:Jest 和 React 测试库
订阅以了解第二篇和第三篇文章。
为什么要进行自动化测试
自动化测试有用的原因有很多,但我最喜欢的原因是:您已经在测试了。
例如,您在页面中添加一个新按钮。然后,您在浏览器中打开此页面并点击此按钮以检查其是否正常工作——这是一个手动测试。通过自动化此过程,您可以确保之前正常工作的功能始终正常运行。
自动化测试对于不常用的功能尤其有用:我们总是测试按钮是否能提交表单,所有字段是否填写正确,但却常常忘记测试隐藏在模态框中、只有老板的老板才会使用的复选框。自动化测试可以确保它仍然有效。
自动化测试的其他原因包括:
有信心更改代码:编写良好的测试使您能够自信地重构代码,确保不会破坏任何东西,并且不会浪费时间更新测试。
文档:测试解释代码如何工作以及预期的行为。与任何书面文档相比,测试始终保持最新状态。
错误和回归预防:通过为应用中发现的每个错误添加测试用例,您可以确保这些错误永远不会再次出现。编写测试可以加深您对代码和需求的理解,您将能够批判性地审视代码,并发现否则可能会忽略的问题。
自动化测试可以在将错误提交到存储库之前捕获错误,而手动测试则不同,在手动测试中,大多数错误都是在测试期间甚至在生产过程中发现的。
测试什么
由Mike Cohn提出的测试金字塔可能是最流行的软件测试方法:
它说 UI 测试是最慢且编写成本最高的,而单元测试是最快的且编写成本最低的,所以我们应该编写大量的单元测试和少量的 UI 测试。
单元测试测试的是单个代码单元,例如函数或 React 组件。运行单元测试不需要浏览器或数据库,因此速度非常快。UI测试测试在真实浏览器中加载的整个应用,通常还会使用真实数据库。这是确保应用所有部分协同工作的唯一方法,但速度慢、编写困难且经常不稳定。服务测试介于两者之间:它们测试多个单元的集成,但不包含任何 UI。
这在后端可能运行良好,但在前端,UI 细节经常会在不改变整体用户流程的情况下发生变化,这导致许多单元测试失败。我们花费大量时间更新单元测试,但对更重要的功能是否仍然有效却缺乏足够的信心。
那么前端可能需要不同的测试方法吗?
Kent C. Dodds推出的测试奖杯在前端测试中越来越受欢迎:
它说集成测试可以给你带来最大的投资回报,所以你应该编写比任何其他类型的测试更多的集成测试。
奖杯中的端到端测试主要对应于金字塔中的 UI 测试。集成测试验证大型功能甚至整个页面,但无需任何后端、真实数据库或真实浏览器。例如,渲染登录页面,输入用户名和密码,点击“登录”按钮,并验证是否发送了正确的网络请求,但实际上并不发起任何网络请求——我们稍后会学习如何操作。
尽管集成测试的编写成本更高,但与单元测试相比,它们有几个好处:
单元测试 | 集成测试 |
---|---|
一次测试仅涵盖一个模块 | 一个测试涵盖整个功能或页面 |
重构后通常需要重写 | 大多数情况下都能通过重构 |
难以避免测试实施细节 | 更好地模仿用户使用你的应用的方式 |
最后一点很重要:集成测试能最大程度地确保我们的应用能够按预期运行。但这并不意味着我们只应该编写集成测试。其他测试也有其适用之处,但我们应该专注于最有用的测试。
现在,让我们从最底层开始仔细看看每个测试奖杯级别:
- 静态分析可以捕获语法错误、不良实践和不正确的 API 使用:
- 代码格式化程序,例如Prettier;
- Linters,例如ESLint;
- 类型检查器,例如TypeScript和Flow。
- 单元测试验证棘手算法是否正常工作。工具:Jest。
- 集成测试可让您确信应用程序的所有功能均按预期运行。工具:Jest和Enzyme或react-testing-library。
- 端到端测试可确保您的应用程序整体运行:前端、后端、数据库以及其他所有内容。工具:Cypress。
我认为 Prettier 也是一种测试工具,因为它经常使错误的代码看起来很奇怪,所以你开始质疑你的代码,仔细阅读它并发现一个错误。
其他类型的测试可能对您的项目也有用。
测试最佳实践
避免测试内部
想象一下,您有一个订阅表单组件:一个电子邮件输入和一个提交按钮,并且您想要测试当用户提交表单时,是否会出现一条成功消息:
test('shows a success message after submission', () => {
const wrapper = mount(<SubscriptionForm />);
wrapper.instance().handleEmailChange('hello@example.com');
wrapper.instance().handleSubmit();
expect(wrapper.state('isSubmitted')).toBe(true);
});
本次测试存在几个问题:
- 如果您改变处理状态的方式(例如,用 Redux 或 hooks 替换 React 状态)甚至重命名状态字段或方法,则此测试将会中断;
- 它没有从用户的角度测试表单是否真正起作用:表单可能没有连接到方法,当 为真时
handleSubmit
可能不会出现成功消息;isSubmitted
第一个问题叫做假阴性:即使行为保持不变,测试仍然失败。这样的测试会让重构变得非常困难,你永远不知道测试失败是因为你破坏了某些东西,还是因为测试本身很糟糕。
第二个问题叫做误报:即使代码有问题,测试也通过了。这样的测试无法保证代码确实对用户有用。
让我们重写测试并解决这两个问题:
test('shows a success message after submission', () => {
const {getByLabelText, getByText, getByRole} = render(<SubscriptionForm />);
fireEvent.change(getByLabelText(/email/i, { target: { value: 'hello@example.com' } });
fireEvent.click(getByText(/submit/i);
expect(getByRole('status').textContent).toMatch('Thank you for subscribing!');
});
有关更多详细信息,请参阅 Kent C. Dodds 的测试实施细节文章。
好的测试验证外部行为是正确的,但不知道任何实现细节。
测试应该是确定性的
非确定性测试是有时通过有时不通过的测试。
一些可能的原因包括:
- 不同的时区;
- 不同的文件系统(不同的路径分隔符);
- 每次测试之前都不会清除并重新填充数据库;
- 状态,在多个测试用例之间共享;
- 依赖于测试用例运行的顺序;
- 用于测试异步行为的超时。
处理非确定性测试的方法有很多,例如轮询、模拟计时器或模拟。我们将在本文后面讨论几个示例。
好的测试是确定性的,它们不依赖于环境。
避免不必要的期望和测试
我经常看到这样的测试:
expect(pizza).toBeDefined();
expect(pizza).toHaveAProperty('cheese', 'Mozarella');
第一个期望是不必要的:如果pizza
未定义,第二个期望无论如何都会失败。Jest 中的错误消息足以让您了解发生了什么。
有时甚至整个测试用例都是不必要的:
test('error modal is visible', () => {});
test('error modal has an error message', () => {});
如果我们知道错误模态框中的错误消息是可见的,那么我们就可以确定模态框本身也是可见的。因此,我们可以放心地删除第一个测试。
好的测试不会有任何不必要的期望或测试用例。
如果您喜欢这篇文章,请订阅我的时事通讯。
不要追求 100% 的代码覆盖率
完整的测试覆盖在理论上听起来是个好主意,但在实践中却行不通。
追求高测试覆盖率存在一些问题:
- 高测试覆盖率会给你一种虚假的安全感。“代码覆盖”意味着代码在测试运行期间被执行,但并不意味着测试实际上验证了这段代码的功能。如果测试覆盖率低于 100%,你可以确定你没有测试某些代码,但即使覆盖率达到 100%,你也无法确定你测试了所有内容。
- 有些功能确实很难测试,比如浏览器中的文件上传或拖放功能。你开始模拟或访问组件内部,因此你的测试不再真实反映用户使用应用的方式,而且难以维护。最终,你开始花费更多时间编写一些用处不大的测试——所谓的收益递减问题。
根据我的经验,100%的测试覆盖率在两种情况下很有用:
- 在库中,避免意外破坏现有 API 的更改至关重要。
- 在开源项目中,大多数更改都是由不熟悉代码库的贡献者完成的。
好的测试易于维护并让您有信心更改代码。
结论
我们介绍了编写前端测试的最重要理论和最佳实践:
- 编写比任何其他类型的测试更多的集成测试。
- 避免测试内部。
- 测试应该是确定性的。
- 避免不必要的期望和测试。
- 不要追求 100% 的代码覆盖率。
现在我们准备开始编写自己的测试了。本系列的接下来两篇文章是互为分支的,所以您可以随意阅读您感兴趣的文章,无论是 Enzyme 还是 React 测试库。如果您还在犹豫,两篇文章的开头都列出了每个库的优缺点:这将帮助您做出选择。
感谢 Joe Boyle、Kent C. Dodds、Patrick Hund、Monica Lent、Morgan Packard、Alexander Plavinski、Giorgio Polvara、Juho Vepsäläinen。
文章来源:https://dev.to/sapegin/modern-react-testing-part-1-best-practices-1o93