这一系列的文章是对React组件和一般的前端测试现状的深入介绍,解释了很多为什么,而不仅仅是怎么做。我们将讨论为什么要写自动化测试,写什么测试以及如何写。在实际文章中,我们将学习如何使用Jest、Enzyme和React测试库来测试React组件。
我在三年前写过一篇类似的文章,现在我看它就像看一本坏习惯的手册。几乎所有我当时推荐的东西,我现在都不做了。
这是一个系列的第一篇文章,我们学习为什么测试自动化是有用的,哪些类型的测试要写,以及测试的最佳实践。
为什么要进行自动化测试
自动化测试有用的原因有很多,但我最喜欢的原因是:你已经在测试了。
例如,你正在向一个页面添加一个新的按钮。然后你在浏览器中打开这个页面,点击这个按钮,检查它是否工作--这是一个手动测试。通过自动化这个过程,你可以确保过去的功能总是能够正常工作。
自动测试对很少使用的功能特别有用:我们总是测试按钮是否在所有字段都正确填写的情况下提交表单,但我们往往忘记测试那个隐藏在模态中,只有你老板的老板使用的复选框。自动化测试将确保它仍然工作。
自动化测试的其他原因是:
改变代码的信心:写得很好的测试允许你有信心重构代码,因为你不会破坏任何东西,而且不需要浪费时间更新测试。
文档:测试解释了代码是如何工作的,预期的行为是什么。测试,与任何书面文档相比,总是最新的。
错误和回归预防:通过为你的应用程序中发现的每一个错误添加测试案例,你可以确保这些错误不会再出现。编写测试将提高你对代码和需求的理解,你将严格审视你的代码,发现你会错过的问题。
自动测试使得在你提交到存储库之前就能捕捉到bug,相比之下,手动测试在测试过程中甚至在生产过程中发现大部分bug。
测试什么
由Mike Cohn提出的测试金字塔,可能是软件测试中最流行的方法。
它说,UI测试是最慢的,也是最昂贵的,而单元测试是最快的,也是最便宜的,所以我们应该写很多单元测试,很少写UI测试。
单元测试是测试单一的代码单元,如一个函数或一个React组件。你不需要浏览器或数据库来运行单元测试,所以它们是非常快的。UI测试是测试在真实浏览器中加载的整个应用程序,通常有一个真实的数据库。这是确保你的应用程序的所有部分一起工作的唯一方法,但它们很慢,写起来很麻烦,而且经常会出现问题。服务测试介于两者之间:它们测试多个单元的集成,但没有任何UI。
这在后端可能很有效,但在前端的UI细节经常改变而不改变更大的用户流,这导致了许多单元测试的失败。我们花了很多时间来更新单元测试,但却没有足够的信心来保证更大的功能仍在工作。
因此,也许前端需要一种不同的测试方法?
由Kent C. Dodds介绍的测试奖杯在前端测试中越来越受欢迎。
它说,集成测试给你最大的投资回报,所以你应该写更多的集成测试,而不是其他类型的测试。
奖杯中的端到端测试大多对应于金字塔中的UI测试。集成测试验证大的功能,甚至整个页面,但没有任何后台,真正的数据库或真正的浏览器。例如,渲染一个登录页面,输入一个用户名和密码,点击 "登录 "按钮,并验证是否发送了正确的网络请求,但没有实际进行任何网络请求 -我们将在后面学习如何做。
即使集成测试的编写成本较高,它们也比单元测试有几个好处:
| 单元测试 | 集成测试 |
|---|---|
| 一个测试只覆盖一个模块 | 一个测试覆盖整个功能或一个页面 |
| 重构后往往需要重写 | 大部分时间都能在重构中生存 |
| 很难避免测试实施细节 | 更好地类似于用户如何使用你的应用程序 |
最后一点很重要:集成测试给了我们最大的信心,让我们的应用程序按照预期运行。但这并不意味着,我们应该只写集成测试。其他测试也有它们的位置,但我们应该把精力放在最有用的测试上。
现在,让我们从最底层开始,仔细看看每个测试的奖杯级别。
- 静态分析可以捕捉到语法错误、不良做法和对API的不正确使用。
- 代码格式化器,如Prettier; - 伪造者,如ESLint; - 类型检查器,如TypeScript和Flow。2.单元测试验证棘手的算法是否正确工作。工具。Jest. 3.3.集成测试让你相信你的应用程序的所有功能都能按预期工作。工具。Jest和Enzyme或react-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测试库。如果你还在选择,这两篇文章都在最开始列出了每个库的优点和缺点:这将有助于你做出选择。