React 组件测试
在上一篇文章中,我们介绍了如何使用 Jest 测试 JavaScript 代码。本篇文章我们将进一步扩展这个主题,介绍如何使用 React 测试库和 Jest 来测试 React 组件。
React 测试库和 Jest 均提供开箱即用的支持create-react-app
,无需单独安装。React 测试库 (RTL) 构建于 DOM 测试库之上,因此测试将与实际的 DOM 交互。这使得测试能够尽可能逼真地模拟用户与 DOM 的交互方式。它非常易于使用,提供了一系列断言方法(扩展自 Jest)、用户事件模拟等功能。
create-react-app
使用 Jest 作为测试运行器。Jest 将按照以下命名约定(根据官方网站)查找测试文件的名称:
- __tests__ 文件夹中带有 .js 后缀的文件。
- 带有 .test.js 后缀的文件。
- 带有 .spec.js 后缀的文件。
今天我们将探索如何渲染待测试的组件,在组件中找到正确的元素,并执行快照测试。让我们开始创建一个新create-react-app
项目:
npx create-react-app testing-react-demo
创建后,将目录更改为您创建的应用程序,然后在所需的代码编辑器中打开该目录。
您应该已经在src文件夹中看到App.test.js。
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
您可以删除此文件,也可以保留它。为了演示,我将删除它,因此您不会在测试套件中看到它。
接下来我通常会创建一个components文件夹,并将属于该组件的文件(例如 css 文件和测试文件)都放进去。创建components文件夹后,再创建两个名为SubscribeForm和PokeSearch的文件夹。这两个就是我们今天要编写测试的组件。
让我们在 SubscribeForm 文件夹中创建第一个简单组件:
SubscribeForm.js
import React, { useState } from 'react';
import "./SubscribeForm.css";
const SubscribeForm = () => {
const [isDisabled, setIsDisabled] = useState(true);
const [email, setEmail] = useState("");
function handleChange(e){
setEmail(e.target.value);
setIsDisabled(e.target.value === "");
}
return (
<div className="container">
<h1>Subscribe To Our Newsletter</h1>
<form className="form">
<label htmlFor="email">Email Address</label>
<input onChange={handleChange} type="email" id="email" name="email" placeholder="Email Address" value={email} />
<input type="checkbox" name="agreement_checkbox" id="agreement_checkbox" />
<label htmlFor="agreement_checkbox">I agree to disagree whatever the terms and conditions are.</label>
<button name="subscribe-button" type="submit" className="button" disabled={isDisabled} >Subscribe</button>
</form>
</div>
);
};
export default SubscribeForm;
这是一个简单的组件,其中有一个用于输入电子邮件地址的输入框和一个用于点击“订阅”的按钮。在输入框中输入任何文本之前,该按钮首先被禁用,以防止点击。这个按钮似乎是我们可以创建的完美测试用例之一。
文本输入后按钮启用
接下来,我们将创建另一个名为 PokeSearch 的组件(我不是 Pokemon 的粉丝,但 Poke API 很适合演示)。另一个简单的例子是,我们有一个包含 useEffect hook 的组件,用于从 API 获取信息,并将其(Pokemon 名称)显示到屏幕上。在获取结果之前,我们会向用户显示“...正在加载...”的文本。
PokeSearch.js
import React, { useEffect, useState } from 'react';
const PokeSearch = () => {
const [pokemon, setPokemon] = useState({});
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetch(`https://pokeapi.co/api/v2/pokemon/bulbasaur`)
.then((res) => res.json())
.then((result) => {
setPokemon(result);
setIsLoading(false);
})
.catch((err) => console.log(err));
}, [])
return (
<div>
{isLoading
? <h3>...Loading...</h3>
: <p>{pokemon.name}</p>
}
</div>
);
}
export default PokeSearch;
让我们开始测试这两个组件。对于第一个组件 SubscribeForm ,我们创建一个名为SubscribeForm.test.js的新文件。我们遵循命名约定,以便测试运行器能够识别它。为了创建测试,我们需要来自Testing-library/reactrender
的和,以及来自Testing-library/user-event 的用户事件。此外,请记住导入我们要测试的组件。screen
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import SubscribeForm from './SubscribeForm';
我们可以首先创建一个测试,以确保在页面首次加载时我们的按钮被禁用,因为电子邮件地址字段中没有输入。
it("The subscribe button is disabled before typing anything in the input text box", () => {
render(<SubscribeForm />);
expect(screen.getByRole("button", {name: /subscribe/i})).toBeDisabled();
});
从上一篇文章中,我们知道我们将为我们的测试命名,并提供一个回调函数,其中包括断言。
首先,我们使用 render 方法将要测试的组件渲染到附加到 document.body 的容器中(顺便提一下,Jest 26 及之前的版本使用 jsdom 作为默认环境)。渲染完组件后,我们需要找到合适的元素(也就是按钮)进行测试。我们可以使用 RTL 中的查询方法来做到这一点。DOM 中的元素可以通过它们的可访问性角色和名称(稍后会详细介绍)、文本或我们赋予元素的测试 id 来找到。官方声明给出了优先级。他们建议通过角色或文本(所有人都可以访问)、语义 HTML(替代文本,例如 img、area 等)和测试 id(用户无法看到或听到这些,因此如果您无法理解使用以前的方法,请使用此方法)进行查询。
<div data-testid="test-element" />
screen.getByTestId('test-element')
您可以在此处找到有关优先级的更多信息:
关于 React Testing Library 的查询
您可以这样做来找出组件内的可访问角色:您只需screen.getByRole("")
为该组件编写测试,它会失败但会为您提供可访问性信息和这些元素的名称。
Here are the accessible roles:
heading:
Name "Subscribe To Our Newsletter":
<h1 />
--------------------------------------------------
textbox:
Name "Email Address":
<input
id="email"
name="email"
placeholder="Email Address"
type="email"
value=""
/>
--------------------------------------------------
checkbox:
Name "I agree to disagree whatever the terms and conditions are.":
<input
id="agreement_checkbox"
name="agreement_checkbox"
type="checkbox"
/>
--------------------------------------------------
button:
Name "Subscribe":
<button
class="button"
disabled=""
name="subscribe-button"
type="submit"
/>
--------------------------------------------------
从这里我们知道我们有不同的可访问性角色,例如按钮、文本框、复选框和标题。为了定位我们的订阅按钮,我们需要定位角色“按钮”。定位角色后,我们具体需要具有可访问名称“订阅”的按钮,如提供的可访问性信息中所述(“名称“订阅”)。“名称”的值可以从元素的可见或不可见属性中派生出来,按钮中的文本就是其中之一。为了搜索它的名称,我们通常将不区分大小写的正则表达式放入 getByRole( {name: /subscribe/i}
) 的第二个对象参数中。获取该按钮后,我们需要检查该按钮是否被禁用(它应该被禁用)。
接下来是第二个测试。在这个测试中,我们模拟用户在文本框中输入内容的事件,并启用按钮。
it("The subscribe button becomes enabled when we start typing in the input text box", () => {
render(<SubscribeForm />);
userEvent.type(screen.getByRole("textbox", {name: /email/i}), "abc@email.com");
expect(screen.getByRole("button", {name: /subscribe/i})).toBeEnabled();
});
我们用同样的步骤将 SubscribeForm 渲染到文档中,并使用“type”用户事件在所需的元素上输入一些文本。在本例中,它是一个文本框,我们可以通过可访问角色和名称进行选择(请参阅我们刚才获取的可访问性信息)。第二个参数userEvent.type()
是要输入的文本。文本输入完成后,我们就可以期待按钮被启用了。
最后,我们正在对 React 组件进行快照测试。我们需要使用 react-test-renderer 为快照渲染一个纯 JavaScript 对象(不依赖于 DOM)。
npm install react-test-renderer
安装并导入后,我们可以使用渲染器在 JavaScript 对象中创建 SubscribeForm 组件。最后,我们使用 Jest 的 toMatchSnapshot() 函数来启动快照测试。
it("Test to match snapshot of component", () => {
const subscribeFormTree = renderer.create(<SubscribeForm />).toJSON();
expect(subscribeFormTree).toMatchSnapshot();
})
当您第一次运行此测试时,它将在您的目录中创建一个新文件夹(运行测试后自动创建),名为__snapshots__,在本例中为 SubscribeForm 文件夹。
PASS src/components/PokeSearch/PokeSearch.test.js
PASS src/components/SubscribeForm/SubscribeForm.test.js
› 1 snapshot written.
Snapshot Summary
› 1 snapshot written from 1 test suite.
Test Suites: 2 passed, 2 total
Tests: 5 passed, 5 total
Snapshots: 1 written, 1 total
Time: 2.519 s
Ran all test suites.
Watch Usage: Press w to show more.
您可以在其中找到一个 snap 文档。
SubscribeForm.test.js.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Test to match snapshot of component 1`] = `
<div
className="container"
>
<h1>
Subscribe To Our Newsletter
</h1>
<form
className="form"
>
<label
htmlFor="email"
>
Email Address
</label>
<input
id="email"
name="email"
onChange={[Function]}
placeholder="Email Address"
type="email"
value=""
/>
<input
id="agreement_checkbox"
name="agreement_checkbox"
type="checkbox"
/>
<label
htmlFor="agreement_checkbox"
>
I agree to disagree whatever the terms and conditions are.
</label>
<button
className="button"
disabled={true}
name="subscribe-button"
type="submit"
>
Subscribe
</button>
</form>
</div>
`;
现在,测试套件已经记录了组件之前的快照。如果您再次运行测试,它将再次拍摄组件的快照,并与__snapshots__文件夹中的快照进行比较。如果它们不同,测试将失败。这有助于确保我们的 UI 组件没有被意外更改。让我们尝试更改 SubscribeForm 组件,然后再次运行测试。我们将把“订阅我们的新闻通讯”更改为“订阅他们的新闻通讯”。
<h1>Subscribe To Their Newsletter</h1>
然后我们再次运行测试。
PASS src/components/PokeSearch/PokeSearch.test.js
FAIL src/components/SubscribeForm/SubscribeForm.test.js
● Test to match snapshot of component
expect(received).toMatchSnapshot()
Snapshot name: `Test to match snapshot of component 1`
- Snapshot - 1
+ Received + 1
@@ -1,10 +1,10 @@
<div
className="container"
>
<h1>
- Subscribe To Our Newsletter
+ Subscribe To Their Newsletter
</h1>
<form
className="form"
>
<label
22 | it("Test to match snapshot of component", () => {
23 | const subscribeFormTree = renderer.create(<SubscribeForm />).toJSON();
> 24 | expect(subscribeFormTree).toMatchSnapshot();
| ^
25 | })
at Object.<anonymous> (src/components/SubscribeForm/SubscribeForm.test.js:24:31)
› 1 snapshot failed.
Snapshot Summary
› 1 snapshot failed from 1 test suite. Inspect your code changes or press `u` to update them.
Test Suites: 1 failed, 1 passed, 2 total
Tests: 1 failed, 4 passed, 5 total
Snapshots: 1 failed, 1 total
Time: 3.817 s
Ran all test suites.
Watch Usage: Press w to show more.
……测试失败了。如果这是我们预期的更改,我们可以按“u”键将快照更新到最新版本。这样一来,__snapshots__文件夹中的快照文件就会更新,所有测试都会重新运行,并且这次测试都会通过。这与我们上次使用的(酶库)非常相似。
PASS src/components/PokeSearch/PokeSearch.test.js
PASS src/components/SubscribeForm/SubscribeForm.test.js
› 1 snapshot updated.
Snapshot Summary
› 1 snapshot updated from 1 test suite.
Test Suites: 2 passed, 2 total
Tests: 5 passed, 5 total
Snapshots: 1 updated, 1 total
Time: 2.504 s
Ran all test suites.
Watch Usage: Press w to show more.
因此,这是测试我们的 SubscribeForm 组件的完整脚本。
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import renderer from 'react-test-renderer';
import SubscribeForm from './SubscribeForm';
it("The subscribe button is disabled before typing anything in the input text box", () => {
render(<SubscribeForm />);
expect(screen.getByRole("button", {name: /subscribe/i})).toBeDisabled();
});
it("The subscribe button becomes enabled when we start typing in the input text box", () => {
render(<SubscribeForm />);
userEvent.type(screen.getByRole("textbox", {name: /email/i}), "abc@email.com");
expect(screen.getByRole("button", {name: /subscribe/i})).toBeEnabled();
});
it("Test to match snapshot of component", () => {
const subscribeFormTree = renderer.create(<SubscribeForm />).toJSON();
expect(subscribeFormTree).toMatchSnapshot();
})
需要注意的是:每次测试后,Jest 都会自动执行清理过程afterEach(cleanup)
(全局注入),以防止内存泄漏。
最后,我们还想异步测试我们的组件(PokeSearch)。
import React from 'react';
import { render,screen,waitForElementToBeRemoved } from '@testing-library/react';
import PokeSearch from './PokeSearch';
it("Loading is shown until the Pokemon is fetched", async () => {
render(<PokeSearch />);
expect(screen.getByText('...Loading...')).toBeInTheDocument();
await waitForElementToBeRemoved(screen.queryByText('...Loading...'));
});
首先,我们可以测试“...Loading...”文本是否正确渲染到屏幕上。我们需要查询包含“...Loading...”的正确元素,并使用断言方法检查它是否在 DOM 中。然后,我们可以使用 RTL 提供的异步函数,解析加载文本元素,使其在获取结果后消失。此外,官方网站也推荐使用queryBy...
断言方法查询元素是否从 DOM 中消失。
测试完“正在加载”文本后,我们就可以测试获取数据后的情况了。在这个测试用例中,我们不想使用真实的 API 进行测试(我们只是想确保组件运行正常),因此我们只需模拟获取函数即可。当 Promise 被解析后,我们会修复获取函数返回的数据。之后,我们将渲染 PokeSearch,并调用获取函数来获取我们模拟的数据。数据返回后,我们将尝试findBy...
(findBy...
用于异步情况)查找包含文本“bulbasaur”的元素,并检查该元素是否在 DOM 中。
it("The Pokemon name is displayed correctly after it has been fetched", async () => {
// Mock the browser fetch function
window.fetch = jest.fn(() => {
const pokemon = { name: 'bulbasaur', weight: 69, height: 7 };
return Promise.resolve({
json: () => Promise.resolve(pokemon),
});
});
render(<PokeSearch />);
const pokemonName = await screen.findByText('bulbasaur');
expect(pokemonName).toBeInTheDocument();
});
希望这能让您了解如何开始测试 React 组件。
请关注我,获取更多关于网页设计、编程和自我提升的文章😊
文章来源:https://dev.to/ohdylan/react-component-testing-54ie