React 中的 Jest 测试初学者指南
在我的上一篇文章《Jest 单元测试入门指南》中,我介绍了如何使用 Jest 测试库在 JavaScript 中进行测试。在本文中,我希望通过一个如何为 React 组件编写基本测试的示例,进一步阐述关于匹配器、期望以及测试实现的目的。
使用 Jest 编写 React 组件测试遵循与包含函数和匹配器的代码块相同的describe
函数结构。然而,我们不需要测试单个 JS 函数的功能,而是需要确保 React 组件能够正确渲染,并且用户与组件的交互能够按预期进行。有关 Jest 测试基本设置及其用途的详细指南,请参阅我之前的文章《Jest 单元测试初学者指南》。test
expect
入门
我们将逐步讲解如何搭建一个基本的 React 应用,其中包含一些交互元素,例如带有递增/递减按钮的计数器,以及一个用于将文本提交到 DOM 的表单。我将在这里逐步讲解 Jest 测试和其余代码的编写,您也可以查看包含所有代码的仓库。
内容
设置应用程序
步骤:
- 创建一个新的 react 应用程序,并
cd
进入该目录。 - 使用时,Jest 会作为 React 的依赖项
npx-create-react-app
与 React 测试库一起安装。React 测试库提供了额外的函数来查找组件的 DOM 节点并进行交互。通过这种方式启动 React 应用时,无需进行额外的安装或设置。
npx create-react-app jest-react-example
cd jest-react-example
违约测试剖析
当使用 创建新的 React 应用时npx-create-react-app
,App.js
文件中会预先填充占位符内容,并且默认包含一个测试文件 - App.test.js
。让我们来看看这个测试文件中发生了什么:
// App.test.js
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom/extend-expect';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
-
我们首先从 React Testing Library 导入两个关键函数:
render
和screen
。Render
是一个函数,用于在内存中构建通常会被渲染为网页的 DOM 树。我们将使用它将组件代码转换为用户交互的格式。Screen
是一个包含多个查询函数的对象,允许我们定位 DOM 中的元素。相比之下,它的功能与 类似querySelector
,但语法略有不同,因为我们不会使用元素的标签/类/ID。
-
下一步导入
userEvent
将允许我们使用目标元素模拟各种用户操作,例如按下按钮、输入等。userEvent的完整文档可以在这里找到。 -
第三个导入
@testing-library/jest-dom/extend-expect
提供了可用于目标元素的附加匹配器。Jest -DOM 的完整文档可在此处找到。 -
最后,我们需要在此文件中导入我们将要测试的组件。
完成这些导入后,我们看到了熟悉的 Jest 测试函数结构。
// Copied from above
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
- 测试函数通过
string
描述测试的参数和带有测试内容的回调函数来调用。 - 回调函数首先通过渲染组件来创建组件的 DOM 树。
getByText
该对象的函数通过screen
正则表达式参数调用。该getByText
函数将返回 DOM 中第一个与正则表达式匹配的元素,并将其保存到变量中以供后续使用。- 回调函数由匹配器语句完成
expect
。在本例中,我们只是简单地声明我们期望上一个查询在文档中找到一个元素。
如果我们使用在本地机器上启动应用程序,npm start
我们可以看到指定的链接文本清晰可见,并且默认测试应该通过。
在我们继续编写自己的测试之前,我们可以通过npm test
在控制台中运行来确认默认测试是否正常工作。
规划测试
按照测试驱动开发,让我们首先定义我们的应用程序应该做什么,为其编写测试,然后实现应该通过测试的代码。
-
将有两个按钮:增加和减少。
- 当点击时,它们应该增加/减少页面上的计数器。
- 计数器永远不应为负数,因此当计数器小于 1 时,应禁用减量按钮。
-
应该有一个带有输入字段和提交按钮的表格。
- 用户应该能够在字段中输入内容,提交后,字段中的文本将显示在屏幕上的列表中。
- 每个列表项都有一个“删除”按钮,按下该按钮即可从屏幕上删除该项目。
描述测试
由于计数器值只是一个数字,我希望确保查询与计数器值匹配,而不是页面上可能存在的其他数字(如果仅使用 则可能发生这种情况getByText()
)。为此,我们可以使用data-testid
类似于id
HTML 中使用的数据集属性。不同之处在于,它data-testid
仅用于测试目的,与 CSS 或其他交互无关。
反测试
测试 #1:
在第一个测试中,我编写了与计数器功能初始计划相符的期望语句。我们期望 DOM 包含两个按钮、计数器标签“Counter:”以及计数器的值。我们还期望在页面首次加载时,计数器的默认文本值为 0,因此,我们的减量按钮应该被禁用,以阻止计数器出现负值。
describe( 'App Counter', () => {
test('Counter Elements should be present', () => {
render(<App />)
const incrementButton = screen.getByText(/Increment/i)
const decrementButton = screen.getByText(/Decrement/i)
const counterLabel = screen.getByText(/Counter:/i)
const counterText = screen.getByTestId("counter-value")
expect(incrementButton).toBeInTheDocument()
expect(incrementButton).toBeEnabled()
expect(decrementButton).toBeInTheDocument()
expect(decrementButton).toBeDisabled()
expect(counterLabel).toBeInTheDocument()
expect(counterText).toHaveTextContent(0)
})
})
测试 #2
对于计数器,我们期望每次按下增量按钮时,计数器值都会增加 1。当计数器值超过零时,减量按钮应该启用。为了模拟按钮按下,我们使用之前导入的对象click()
中的函数。userEvent
// Within the describe block from test #1
test('Increment increases value by 1 and enables decrement button present', () => {
render(<App />)
const incrementButton = screen.getByText(/Increment/i)
const decrementButton = screen.getByText(/Decrement/i)
const counterText = screen.getByTestId("counter-value")
expect(counterText).toHaveTextContent(0)
userEvent.click(incrementButton)
expect(counterText).toHaveTextContent(1)
expect(decrementButton).not.toBeDisabled()
})
js
测试 #3
我们期望当按下递减按钮时,计数器值应减少 1。当计数器达到零时,递减按钮应被禁用。
// Within the describe block from test #1
test('Decrement decreases value by 1 and disables decrement button at 0', () => {
render(<App />)
const incrementButton = screen.getByText(/Increment/i)
const decrementButton = screen.getByText(/Decrement/i)
const counterText = screen.getByTestId("counter-value")
expect(counterText).toHaveTextContent(0)
userEvent.click(incrementButton)
expect(counterText).toHaveTextContent(1)
expect(decrementButton).not.toBeDisabled()
userEvent.click(decrementButton)
expect(counterText).toHaveTextContent(0)
expect(decrementButton).toBeDisabled()
})
表格测试
我们的小程序的第二个功能是探索如何测试用户与表单的交互,涉及提交时创建列表项的表单。
测试 #4
首先,我们可以创建基本测试来确保预期的元素呈现到页面上,类似于之前所做的。
describe('App Item List', () => {
test('List Form Components render', () => {
render(<App />)
const listItemInput = screen.getByLabelText(/Create List Item/i)
const addItemButton = screen.getByTestId("add-item")
expect(listItemInput).toBeInTheDocument()
expect(addItemButton).toBeInTheDocument()
})
测试 #6
现在我们已经确认元素存在,我们需要确保它们按预期运行:
- 最初,我们期望输入字段为空,并且用户能够在字段中输入并更改字段的值。
- 对于字段中的文本,我们希望用户能够单击提交按钮以使用该文本在页面上创建新的列表项,并且它将重置输入字段。
test('User can add item to page', () => {
render(<App />)
const listItemInput = screen.getByLabelText(/Create List Item/i)
const addItemButton = screen.getByTestId("add-item")
expect(listItemInput).toHaveValue("")
userEvent.type(listItemInput, "hello")
expect(listItemInput).toHaveValue("hello")
userEvent.click(addItemButton)
expect(screen.getByText("hello")).toBeInTheDocument()
expect(listItemInput).toHaveValue("")
})
测试 #7
创建列表项后,用户应该能够单击旁边的删除按钮,将其从页面中删除。
test('User can remove item from page', () => {
render(<App />)
const listItemInput = screen.getByLabelText(/Create List Item/i)
const addItemButton = screen.getByTestId("add-item")
userEvent.type(listItemInput, "hello")
userEvent.click(addItemButton)
const newItem = screen.getByText("hello")
expect(newItem).toBeInTheDocument()
const removeButton = screen.getByTestId('remove-item0')
userEvent.click(removeButton)
expect(newItem).not.toBeInTheDocument()
})
实现组件
测试完成后,我们现在应该构建组件,它应该能够满足测试中设定的期望。编写组件代码与没有测试时没有什么不同。我们唯一需要做的就是在data-testid
测试查询的元素(getByTestId()
例如列表项和按钮)上添加 。创建组件的完整代码可以在演示下方找到。
我们现在可以使用以下命令运行测试npm test
并查看结果!
下面是使用钩子创建上面演示的组件的代码:
import { useState } from 'react'
import './App.css';
function App() {
const [counter, setCounter] = useState(0)
const [listItems, setListItems] = useState([])
const [newItemText, setNewItemText] = useState("")
const handleCounterClick = value => {
setCounter( counter => counter + value )
}
const handleNewItemChange = e => {
setNewItemText(e.target.value)
}
const handleAddItem = e => {
e.preventDefault()
setListItems([...listItems, {
text: newItemText,id: listItems.length
}
])
setNewItemText('')
}
const handleRemoveItem = id => {
const newListItems = listItems.filter( item => item.id !== id)
setListItems(newListItems)
}
const listItemComponents = listItems.map( item => {
return (
<li
data-testid={`item${item.id}`}
key={item.id}
>
{item.text}
<button
data-testid={`remove-item${item.id}`}
onClick={() => handleRemoveItem(item.id)}
>
Remove
</button>
</li>
)
})
return (
<div className="App">
<header className="App-header">
<p>
Counter:
<span data-testid="counter-value">
{counter}
</span>
</p>
<div>
<button
onClick={() => handleCounterClick(1)}
>
Increment
</button>
<button
onClick={() => handleCounterClick(-1)}
disabled={counter <= 0}
>
Decrement
</button>
</div>
<form onSubmit={handleAddItem}>
<label
htmlFor="newItem"
>
Create List Item
<input
id="newItem"
value={newItemText}
onChange={handleNewItemChange}
/>
</label>
<input
data-testid="add-item"
type="submit"
value="Add Item"
/>
</form>
<ul>
{listItemComponents}
</ul>
</header>
</div>
);
}
export default App;
结论:
虽然这只是触及了测试 React 组件的表面,但我希望这可以作为开始为组件开发自己的测试的入门。