现代React测试第一部分

155 阅读10分钟

这一系列的文章是对React组件和一般的前端测试现状的深入介绍,解释了很多为什么,而不仅仅是怎么做。我们将讨论为什么要写自动化测试,写什么测试以及如何写。在实际文章中,我们将学习如何使用Jest、Enzyme和React测试库来测试React组件。

我在三年前写过一篇类似的文章,现在我看它就像看一本坏习惯的手册。几乎所有我当时推荐的东西,我现在都不做了。

这是一个系列的第一篇文章,我们学习为什么测试自动化是有用的,哪些类型的测试要写,以及测试的最佳实践。

为什么要进行自动化测试

自动化测试有用的原因有很多,但我最喜欢的原因是:你已经在测试了

例如,你正在向一个页面添加一个新的按钮。然后你在浏览器中打开这个页面,点击这个按钮,检查它是否工作--这是一个手动测试。通过自动化这个过程,你可以确保过去的功能总是能够正常工作。

自动测试对很少使用的功能特别有用:我们总是测试按钮是否在所有字段都正确填写的情况下提交表单,但我们往往忘记测试那个隐藏在模态中,只有你老板的老板使用的复选框。自动化测试将确保它仍然工作。

自动化测试的其他原因是:

改变代码的信心:写得很好的测试允许你有信心重构代码,因为你不会破坏任何东西,而且不需要浪费时间更新测试。

文档:测试解释了代码是如何工作的,预期的行为是什么。测试,与任何书面文档相比,总是最新的。

错误和回归预防:通过为你的应用程序中发现的每一个错误添加测试案例,你可以确保这些错误不会再出现。编写测试将提高你对代码和需求的理解,你将严格审视你的代码,发现你会错过的问题。

自动测试使得在你提交到存储库之前就能捕捉到bug,相比之下,手动测试在测试过程中甚至在生产过程中发现大部分bug。

测试什么

Mike Cohn提出的测试金字塔,可能是软件测试中最流行的方法。

它说,UI测试是最慢的,也是最昂贵的,而单元测试是最快的,也是最便宜的,所以我们应该写很多单元测试,很少写UI测试。

单元测试是测试单一的代码单元,如一个函数或一个React组件。你不需要浏览器或数据库来运行单元测试,所以它们是非常快的。UI测试是测试在真实浏览器中加载的整个应用程序,通常有一个真实的数据库。这是确保你的应用程序的所有部分一起工作的唯一方法,但它们很慢,写起来很麻烦,而且经常会出现问题。服务测试介于两者之间:它们测试多个单元的集成,但没有任何UI。

这在后端可能很有效,但在前端的UI细节经常改变而不改变更大的用户流,这导致了许多单元测试的失败。我们花了很多时间来更新单元测试,但却没有足够的信心来保证更大的功能仍在工作。

因此,也许前端需要一种不同的测试方法?

由Kent C. Dodds介绍的测试奖杯在前端测试中越来越受欢迎。

它说,集成测试给你最大的投资回报,所以你应该写更多的集成测试,而不是其他类型的测试。

奖杯中的端到端测试大多对应于金字塔中的UI测试。集成测试验证大的功能,甚至整个页面,但没有任何后台,真正的数据库或真正的浏览器。例如,渲染一个登录页面,输入一个用户名和密码,点击 "登录 "按钮,并验证是否发送了正确的网络请求,但没有实际进行任何网络请求 -我们将在后面学习如何做。

即使集成测试的编写成本较高,它们也比单元测试有几个好处:

单元测试集成测试
一个测试只覆盖一个模块一个测试覆盖整个功能或一个页面
重构后往往需要重写大部分时间都能在重构中生存
很难避免测试实施细节更好地类似于用户如何使用你的应用程序

最后一点很重要:集成测试给了我们最大的信心,让我们的应用程序按照预期运行。但这并不意味着,我们应该只写集成测试。其他测试也有它们的位置,但我们应该把精力放在最有用的测试上。

现在,让我们从最底层开始,仔细看看每个测试的奖杯级别。

  1. 静态分析可以捕捉到语法错误、不良做法和对API的不正确使用。

- 代码格式化器,如Prettier; - 伪造者,如ESLint; - 类型检查器,如TypeScriptFlow。2.单元测试验证棘手的算法是否正确工作。工具。Jest. 3.3.集成测试让你相信你的应用程序的所有功能都能按预期工作。工具。JestEnzymereact-testing-library。4.端到端测试确保你的应用程序作为一个整体工作:前端、后端、数据库和其他一切。工具。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或钩子替换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测试库。如果你还在选择,这两篇文章都在最开始列出了每个库的优点和缺点:这将有助于你做出选择。