对于像使用 React 构建的交互式网站,集成测试是自然而然的选择。它们验证用户与应用程序的交互方式,而无需端到端测试的额外开销。
本文通过一个练习来阐述,该练习从一个简单的网站开始,使用单元测试和集成测试验证行为,并演示集成测试如何通过更少的代码行实现更大的价值。本文内容假设您熟悉 React 和 JavaScript 中的测试。熟悉 Jest 和 React Testing Library 会有所帮助,但不是必需的。
测试分为三种类型:
- 单元测试独立验证一段代码。它们易于编写,但可能会忽略大局。
- 端到端测试 (E2E) 使用自动化框架(例如 Cypress 或 Selenium)像用户一样与您的网站交互:加载页面、填写表单、点击按钮等。它们通常编写和运行速度较慢,但与真实的 用户体验非常接近。
- 集成测试介于两者之间。它们验证应用程序的多个单元如何协同工作,但比 E2E 测试更轻量级。例如,Jest 自带一些内置实用程序来促进集成测试;Jest 在后台使用 jsdom 来模拟常见的浏览器 API,其开销小于自动化,并且其强大的模拟工具可以模拟外部 API 调用。
另一个需要注意的地方:在 React 应用程序中,单元测试和集成测试的 编写方式相同,使用的工具也相同。
开始进行 React 测试
我创建了一个简单的 React 应用程序(可在 GitHub 上找到),其中包含一个登录表单。我将其连接到 reqres.in,这是一个我发现用于测试前端项目的方便的 API。
您可以成功登录:
…或者遇到来自 API 的错误消息:
代码结构如下:
<code>LoginModule/ ├── components/ │ ├── Login.js // 渲染 LoginForm、错误消息和登录确认 │ └── LoginForm.js // 渲染登录表单字段和按钮 ├── hooks/ │ └── useLogin.js // 连接到 API 并管理状态 └── index.js // 将所有内容整合在一起</code>
选项 1:单元测试
如果您像我一样喜欢编写测试——也许戴着耳机,在 Spotify 上播放着不错的音乐——那么您可能会忍不住为每个文件编写单元测试。
即使您不是测试爱好者,您也可能正在参与一个“试图做好测试”的项目,但没有明确的策略,测试方法是“我想每个文件都应该有自己的测试?”
这看起来像这样(为了清晰起见,我在测试文件名中添加了 unit):
<code>LoginModule/ ├── components/ │ ├── Login.js │ ├── Login.unit.test.js │ ├── LoginForm.js │ └── LoginForm.unit.test.js ├── hooks/ │ ├── useLogin.js │ └── useLogin.unit.test.js ├── index.js └── index.unit.test.js</code>
我在 GitHub 上完成了添加所有这些单元测试的练习,并创建了一个 test:coverage:unit 脚本以生成覆盖率报告(Jest 的内置功能)。我们可以通过四个单元测试文件实现 100% 的覆盖率:
100% 的覆盖率通常是过度的,但对于如此简单的代码库来说是可以实现的。
让我们深入研究为 onLogin React hook 创建的单元测试之一。如果您不熟悉 React hook 或如何测试它们,请不要担心。
test('successful login flow', async () => { // 模拟成功的 API 响应 jest .spyOn(window, 'fetch') .mockResolvedValue({ json: () => ({ token: '123' }) }); const { result, waitForNextUpdate } = renderHook(() => useLogin()); act(() => { result.current.onSubmit({ email: '[email protected]', password: 'password', }); }); // 将状态设置为 pending expect(result.current.state).toEqual({ status: 'pending', user: null, error: null, }); await waitForNextUpdate(); // 将状态设置为 resolved,存储电子邮件地址 expect(result.current.state).toEqual({ status: 'resolved', user: { email: '[email protected]', }, error: null, }); });
这个测试写起来很有趣(因为 React Hooks Testing Library 使测试 hook 变得轻而易举),但它有一些问题。
首先,测试验证内部状态从 'pending' 更改为 'resolved';此实现细节不会向用户公开,因此,可能不是要测试的好东西。如果我们重构应用程序,我们将不得不更新此测试,即使从用户的角度来看没有任何变化。
此外,作为单元测试,这只是其中一部分。如果我们想验证登录流程的其他功能,例如提交按钮文本更改为“加载中”,我们将不得不在不同的测试文件中进行操作。
选项 2:集成测试
让我们考虑添加一个集成测试来验证此流程的替代方法:
<code>LoginModule/ ├── components/ │ ├── Login.js │ └── LoginForm.js ├── hooks/ │ └── useLogin.js ├── index.js └── index.integration.test.js</code>
我实现了这个测试和一个 test:coverage:integration 脚本以生成覆盖率报告。就像单元测试一样,我们可以达到 100% 的覆盖率,但这次都在一个文件中,并且需要的代码行更少。
以下是涵盖成功登录流程的集成测试:
test('successful login', async () => { jest .spyOn(window, 'fetch') .mockResolvedValue({ json: () => ({ token: '123' }) }); render(<loginmodule></loginmodule>); const emailField = screen.getByRole('textbox', { name: 'Email' }); const passwordField = screen.getByLabelText('Password'); const button = screen.getByRole('button'); // 填写并提交表单 fireEvent.change(emailField, { target: { value: '[email protected]' } }); fireEvent.change(passwordField, { target: { value: 'password' } }); fireEvent.click(button); // 它设置加载状态 expect(button).toBeDisabled(); expect(button).toHaveTextContent('Loading...'); await waitFor(() => { // 它隐藏表单元素 expect(button).not.toBeInTheDocument(); expect(emailField).not.toBeInTheDocument(); expect(passwordField).not.toBeInTheDocument(); // 它显示成功文本和电子邮件地址 const loggedInText = screen.getByText('Logged in as'); expect(loggedInText).toBeInTheDocument(); const emailAddressText = screen.getByText('[email protected]'); expect(emailAddressText).toBeInTheDocument(); }); });
我真的很喜欢这个测试,因为它从用户的角度验证了整个登录流程:表单、加载状态和成功确认消息。集成测试非常适合 React 应用程序,正是因为这种用例;用户体验是我们想要测试的 内容,而这几乎总是涉及 多个不同的代码片段协同工作。
此测试不了解使预期行为起作用的组件或 hook,这很好。只要用户体验保持不变,我们就可以重写和重构这些实现细节而不会破坏测试。
我不会深入研究登录流程的初始状态和错误处理的其他集成测试,但我鼓励您在 GitHub 上查看它们。
那么,什么 需要 单元测试?
与其考虑单元测试与集成测试,不如让我们退一步,考虑一下我们如何决定首先需要测试什么。需要测试 LoginModule,因为它是一个我们希望使用者(应用程序中的其他文件)能够放心地使用的实体。
另一方面,不需要测试 onLogin hook,因为它只是 LoginModule 的实现细节。但是,如果我们的需求发生变化,并且 onLogin 在其他地方有用例,那么我们将需要添加我们自己的(单元)测试来验证其作为可重用实用程序的功能。(我们也需要移动该文件,因为它不再特定于 LoginModule 了。)
单元测试仍然有很多用例,例如需要验证可重用选择器、hook 和普通函数。在开发代码时,您可能还会发现使用单元测试进行 测试驱动开发 很有帮助,即使您稍后将该逻辑向上移动到集成测试。
此外,单元测试在针对多个输入和用例进行详尽测试方面做得很好。例如,如果我的表单需要针对各种场景(例如无效电子邮件、缺少密码、密码过短)显示内联验证,我将在集成测试中涵盖一个代表性案例,然后在单元测试中深入研究具体案例。
其他好处
既然我们在这里,我想谈谈一些帮助我的集成测试保持清晰和有序的语法技巧。
清晰的 waitFor 块
我们的测试需要考虑 LoginModule 的加载状态和成功状态之间的延迟:
const button = screen.getByRole('button'); fireEvent.click(button); expect(button).not.toBeInTheDocument(); // 太快了,按钮还在!
我们可以使用 DOM Testing Library 的 waitFor 辅助函数来做到这一点:
const button = screen.getByRole('button'); fireEvent.click(button); await waitFor(() => { expect(button).not.toBeInTheDocument(); // 啊,好多了 });
但是,如果我们还想测试其他一些项目呢?网上没有很多关于如何处理此问题的好的示例,并且在过去的项目中,我已经将其他项目放在 waitFor 之外:
// 等待按钮 await waitFor(() => { expect(button).not.toBeInTheDocument(); }); // 然后测试确认消息 const confirmationText = getByText('Logged in as [email protected]'); expect(confirmationText).toBeInTheDocument();
这有效,但我不喜欢它,因为它使按钮条件看起来很特殊,即使我们可以轻松地切换这些语句的顺序:
// 等待确认消息 await waitFor(() => { const confirmationText = getByText('Logged in as [email protected]'); expect(confirmationText).toBeInTheDocument(); }); // 然后测试按钮 expect(button).not.toBeInTheDocument();
在我看来,将与相同更新相关的所有内容一起分组到 waitFor 回调中要好得多:
await waitFor(() => { expect(button).not.toBeInTheDocument(); const confirmationText = screen.getByText('Logged in as [email protected]'); expect(confirmationText).toBeInTheDocument(); });
对于像这样的简单断言,我真的很喜欢这种技术,但在某些情况下,它可能会减慢测试速度,等待在 waitFor 之外立即发生的失败。有关此示例,请参阅 React Testing Library 常用错误中的“在单个 waitFor 回调中有多个断言”。
对于包含几个步骤的测试,我们可以连续使用多个 waitFor 块:
const button = screen.getByRole('button'); const emailField = screen.getByRole('textbox', { name: 'Email' }); // 填写表单 fireEvent.change(emailField, { target: { value: '[email protected]' } }); await waitFor(() => { // 检查按钮是否已启用 expect(button).not.toBeDisabled(); expect(button).toHaveTextContent('Submit'); }); // 提交表单 fireEvent.click(button); await waitFor(() => { // 检查按钮是否不再存在 expect(button).not.toBeInTheDocument(); });
如果您只等待一个项目出现,则可以使用 findBy 查询代替。它在后台使用 waitFor。
行内 it 注释
另一个测试最佳实践是编写更少、更长的测试;这使您可以将测试用例与重要的用户流程关联起来,同时使测试保持隔离,以避免意外行为。我赞成这种方法,但它在保持代码组织和记录所需行为方面可能会带来挑战。我们需要未来的开发人员能够返回测试并了解它在做什么,为什么它会失败等等。
例如,假设这些期望之一开始失败:
it('handles a successful login flow', async () => { // 为清晰起见隐藏测试的开头 expect(button).toBeDisabled(); expect(button).toHaveTextContent('Loading...'); await waitFor(() => { expect(button).not.toBeInTheDocument(); expect(emailField).not.toBeInTheDocument(); expect(passwordField).not.toBeInTheDocument(); const confirmationText = screen.getByText('Logged in as [email protected]'); expect(confirmationText).toBeInTheDocument(); }); });
查看此内容的开发人员无法轻松确定正在测试的内容,并且可能难以确定失败是 错误(这意味着我们应该修复代码)还是 行为更改(这意味着我们应该修复测试)。
我最喜欢的解决方案是使用每个测试的鲜为人知的测试语法,并添加描述正在测试的每个关键行为的行内 it 样式注释:
test('successful login', async () => { // 为清晰起见隐藏测试的开头 // 它设置加载状态 expect(button).toBeDisabled(); expect(button).toHaveTextContent('Loading...'); await waitFor(() => { // 它隐藏表单元素 expect(button).not.toBeInTheDocument(); expect(emailField).not.toBeInTheDocument(); expect(passwordField).not.toBeInTheDocument(); // 它显示成功文本和电子邮件地址 const confirmationText = screen.getByText('Logged in as [email protected]'); expect(confirmationText).toBeInTheDocument(); }); });
这些注释不会神奇地与 Jest 集成,因此如果您遇到失败,失败的测试名称将对应于您传递给测试标签的参数,在本例中为“successful login”。但是,Jest 的错误消息包含周围的代码,因此这些 it 注释仍然有助于识别失败的行为。当我从一个期望中删除 not 时,我收到了以下错误消息:
为了获得更明确的错误,有一个名为 jest-expect-message 的包允许您为每个期望定义错误消息:
expect(button, 'button is still in document').not.toBeInTheDocument();
一些开发人员更喜欢这种方法,但我发现它在大多数情况下有点 太 granular 了,因为单个 it 通常涉及多个期望。
团队的后续步骤
有时我希望我们可以为人类制定 linter 规则。如果是这样,我们可以为我们的团队设置一个 prefer-integration-tests 规则,然后就结束了。
但是,唉,我们需要找到一个更类似的解决方案来鼓励开发人员在某些情况下选择集成测试,例如我们前面介绍的 LoginModule 示例。像大多数事情一样,这归结于团队讨论您的测试策略,就对项目有意义的内容达成一致,并且——希望——在 ADR 中记录它。
在制定测试计划时,我们应该避免一种会迫使开发人员为每个文件编写测试的文化。开发人员需要能够放心地做出明智的测试决策,而不必担心他们“测试不足”。Jest 的覆盖率报告可以通过提供一个健全性检查来帮助解决这个问题,即使测试在集成级别上进行了合并。
我仍然不认为自己是集成测试专家,但是进行这项练习帮助我分解了一个集成测试比单元测试提供更大价值的用例。我希望与您的团队分享这一点,或者在您的代码库上进行类似的练习,将有助于指导您将集成测试纳入您的工作流程。
以上是反应集成测试:覆盖范围更大,测试较少的详细内容。更多信息请关注PHP中文网其他相关文章!

前几天我得到了这个问题。我的第一个想法是:奇怪的问题!特异性是关于选择者的,而在符号不是选择器,那么...无关紧要?

在这篇文章中,我们将使用我构建和部署的电子商务商店演示来进行Netlify,以展示如何为传入数据制作动态路线。这是一个公平的


热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

AI Hentai Generator
免费生成ai无尽的。

热门文章

热工具

MinGW - 适用于 Windows 的极简 GNU
这个项目正在迁移到osdn.net/projects/mingw的过程中,你可以继续在那里关注我们。MinGW:GNU编译器集合(GCC)的本地Windows移植版本,可自由分发的导入库和用于构建本地Windows应用程序的头文件;包括对MSVC运行时的扩展,以支持C99功能。MinGW的所有软件都可以在64位Windows平台上运行。

Dreamweaver CS6
视觉化网页开发工具

WebStorm Mac版
好用的JavaScript开发工具

ZendStudio 13.5.1 Mac
功能强大的PHP集成开发环境

记事本++7.3.1
好用且免费的代码编辑器