如何使用 React Testing Library 来测试组件表面
距离我第一次发表这篇文章已经过去好几个月了。一位细心的读者注意到,这个包已经改名了。最新的 API 和安装说明请查看React Testing Library。
在Twitter上关注我,很高兴接受您对主题或改进的建议/Chris
React 测试库是一个与众不同的测试库,它测试的是组件的表面而不是内部。你可以随意更改组件,只要它们以相同的方式渲染数据,或者在交互(例如填写数据或按下按钮)后以相同的方式渲染 React 即可。
图书馆作者 Kent C. Dodds 对此是这样评价的:
简单而完整的 React DOM 测试实用程序,鼓励良好的测试实践
它是测试 React 组件的轻量级解决方案。它基于 提供了实用函数react-dom
。您的测试针对 DOM 节点进行,而不是 React 组件实例。
在本文中,我们将介绍以下内容:
- 编写测试,展示编写测试、实例化组件并对其进行断言是多么简单
- 处理事件,我们将学习如何触发事件并在随后对结果组件进行断言
- 异步操作,我们将学习如何触发和等待异步操作完成
- 管理输入,我们将学习如何将按键发送到组件上的输入元素并对结果进行断言
开始使用非常简单,您只需安装react-testing-library
:
yarn add @testing-library/react
编写测试
让我们看一个真实的场景来理解我们的意思。我们将创建:
Todos.js
允许您呈现列表Todos
并选择特定Todo item
Todos.test.js
,我们的测试文件
我们的组件代码如下所示:
// Todos.js
import React from 'react';
import './Todos.css';
const Todos = ({ todos, select, selected }) => (
<React.Fragment>
{todos.map(todo => (
<React.Fragment key={todo.title}>
<h3 data-testid="item" className={ selected && selected.title === todo.title ? 'selected' :'' }>{todo.title}</h3>
<div>{todo.description}</div>
<button onClick={() => select(todo)}>Select</button>
</React.Fragment>
))}
</React.Fragment>
);
class TodosContainer extends React.Component {
state = {
todo: void 0,
}
select = (todo) => {
this.setState({
todo,
})
}
render() {
return (
<Todos { …this.props } select={this.select} selected={this.state.todo} />
);
}
}
export default TodosContainer;
现在进行测试:
// Todos.test.js
import {render, fireEvent, wait} from 'react-testing-library';
import 'react-testing-library/cleanup-after-each';
import React from 'react';
import 'jest-dom/extend-expect';
import Todos from '../Todos';
const todos = [
{
title: 'todo1'
},
{
title: 'todo2'
}];
describe('Todos', () => {
it('finds title', () => {
const {getByText, getByTestId, container} = render(<Todos todos={todos} />);
})
});
从上面的代码中我们可以看到,我们正在使用一些帮助程序react-testing-library
:
- render(),这将渲染我们的组件
- fireEvent,这将帮助我们触发诸如点击事件或更改输入数据之类的事情
- 等待,这允许我们等待元素出现
查看测试本身,我们发现当我们调用 render 时,我们得到一个对象,并且我们从中解构了 3 个值:
const {getByText, getByTestId, container} = render(<Todos todos={todos} />)
我们最终得到了以下帮助:
- getByText,通过文本内容获取元素
- getByTestId,它通过以下方式获取元素
data-testid
,因此,如果您的元素上有一个属性,那么data-testid="saved"
您可以像这样查询它getByTestId('saved')
- 容器,即你的组件被渲染到的 div
让我们填写该测试:
// Todos.test.js
import {render, fireEvent, wait} from 'react-testing-library';
import React from 'react';
import 'jest-dom/extend-expect';
import 'react-testing-library/cleanup-after-each';
import Todos from '../Todos';
const todos = [
{
title: 'todo1'
},
{
title: 'todo2'
}];
describe('Todos', () => {
it('finds title', () => {
const {getByText, getByTestId, container} = render(<Todos todos={todos} />);
const elem = getByTestId('item');
expect(elem.innerHTML).toBe('todo1');
})
});
如上所示,我们能够使用容器和 querySelector 渲染组件并查询 h3 元素。最后,我们对元素内的文本进行断言。
处理操作
让我们再看一下我们的组件。或者更确切地说,让我们看一下它的摘录:
// excerpt of Todos.js
const Todos = ({ todos, select, selected }) => (
<React.Fragment>
{todos.map(todo => (
<React.Fragment key={todo.title}>
<h3 className={ selected && selected.title === todo.title ? 'selected' :'' }>{todo.title}</h3>
<div>{todo.description}</div>
<button onClick={() => select(todo)}>Select</button>
</React.Fragment>
))}
</React.Fragment>
);
上面我们看到,我们尝试将 CSS 类设置为“selected
当待办事项被选中时”。获取todo
选中状态的方法是点击它,我们可以看看当点击渲染的按钮时如何调用 select 方法(每个待办事项对应一个按钮)。让我们尝试通过添加一个测试来测试一下:
import {render, fireEvent, wait} from 'react-testing-library'
import React from 'react';
import 'jest-dom/extend-expect'
import 'react-testing-library/cleanup-after-each'
import Todos from '../Todos';
const todos = [
{
title: 'todo1'
},
{
title: 'todo2'
}
];
describe('Todos', () => {
it('finds title', () => {
const {getByText, getByTestId, container} = render(<Todos todos={todos} />);
const elem = getByTestId('item');
expect(elem.innerHTML).toBe('todo1');
})
it('select todo', () => {
const {getByText, getByTestId, container} = render(<Todos todos={todos} />);
fireEvent.click(getByText('Select'));
const elem = getByTestId('item');
expect(elem.classList[0]).toBe('selected');
})
});
我们最后一个新添加的测试是使用fireEvent
辅助函数执行 a 操作click
,我们可以看到我们正在使用getByText
辅助函数来查找按钮。我们再次使用容器来查找并断言所选的 CSS 类。
异步测试和输入处理
到目前为止,我们已经向您展示了如何渲染组件、查找结果元素以及对其进行断言。我们还展示了如何执行诸如点击按钮之类的操作。在本节中,我们将展示两件事:
- 处理输入
- 处理异步操作
我们将构建以下内容:
- Note.js是一个允许我们输入数据并保存结果的组件,它还允许我们获取数据
- tests /Note.js,测试文件
让我们看一下该组件:
// Note.js
import React from 'react';
class Note extends React.Component {
state = {
content: '',
saved: '',
};
onChange = (evt) => {
this.setState({
content: evt.target.value,
});
console.log('updating content');
}
save = () => {
this.setState({
saved: `Saved: ${this.state.content}`,
});
}
load = () => {
var me = this;
setTimeout(() => {
me.setState({
data: [{ title: 'test' }, { title: 'test2' }]
})
}, 3000);
}
render() {
return (
<React.Fragment>
<label htmlFor="change">Change text</label>
<input id="change" placeholder="change text" onChange={this.onChange} />
<div data-testid="saved">{this.state.saved}</div>
{this.state.data &&
<div data-testid="data">
{this.state.data.map(item => (
<div className="item" >{item.title}</div>
))}
</div>
}
<div>
<button onClick={this.save}>Save</button>
<button onClick={this.load}>Load</button>
</div>
</React.Fragment>
);
}
}
export default Note;
处理用户输入
为了在我们的示例应用程序中保存数据,我们在输入中输入文本并按保存按钮。
让我们为此创建一个测试:
// __tests__/Note.js
import {render, fireEvent, wait} from 'react-testing-library'
import React from 'react';
import 'jest-dom/extend-expect'
import 'react-testing-library/cleanup-after-each'
import Select from '../Note';
describe('Note', () => {
it('save text', async() => {
const {getByText, getByTestId, getByPlaceholderText, container, getByLabelText} = render(<Select />);
const input = getByLabelText('Change text');
input.value= 'input text';
fireEvent.change(input);
fireEvent.click(getByText('Save'));
console.log('saved', getByTestId('saved').innerHTML);
expect(getByTestId('saved')).toHaveTextContent('input text')
})
});
上面我们可以看到,我们使用了辅助函数getByLabelText
来获取输入的引用,然后我们只需执行input.value = 'input text'
此操作即可。接下来,我们需要调用该方法fireEvent.change(input)
来实现更改。之后,我们可以通过输入以下内容对结果进行断言:expect(getByTestId('saved')).toHaveTextContent('input text')
处理异步代码
我们的组件中还有另一项功能,即按下“加载”按钮即可调用load()
方法,如下所示:
load = () => {
var me = this;
setTimeout(() => {
me.setState({
data: [{ title: 'test' }, { title: 'test2' }]
})
}, 3000);
}
我们可以在上面看到,更改不会立即生效,这是因为我们使用了 setTimeout()。查看我们的组件,我们可以看到,除非将 data 属性设置为某个值,否则我们不会渲染它:
{this.state.data &&
<div data-testid="data">
{this.state.data.map(item => (
<div className="item" >{item.title}</div>
))}
</div>
}
我们的测试需要满足这一点,等待带有属性的 divdata-testid="data"
出现后才能对其进行断言。这可以通过 async/await 来处理。我们导入waitForElement
from react-testing-library
,这样我们就可以在等待元素出现时暂停执行。让我们通过在测试文件中添加一个测试来看一下效果:
import {
render,
fireEvent,
wait,
waitForElement,
} from 'react-testing-library'
import 'react-testing-library/cleanup-after-each';
import React from 'react';
import 'jest-dom/extend-expect'
import Select from '../Note';
describe('Note', () => {
it('save text', async () => {
const {getByText, getByTestId, getByPlaceholderText, container, getByLabelText} = render(<Select />);
const input = getByLabelText('Change text');
input.value= 'input text';
fireEvent.change(input);
fireEvent.click(getByText('Save'));
console.log('saved', getByTestId('saved').innerHTML);
expect(getByTestId('saved')).toHaveTextContent('input text')
})
it('load data', async() => {
const {getByText, getByTestId, getByPlaceholderText, container} = render(<Select />);
fireEvent.click(getByText('Load'));
const elem = await waitForElement(() => getByTestId('data'))
const elem = getByTestId('item');
expect(elem).toHaveTextContent('test');
})
});
await waitForElement(() => getByTestId('data'))
上面我们看到了阻止测试继续执行直到元素出现的结构。它waitForElement
返回一个 Promise,该 Promise 直到元素在 DOM 上存在才会解析。之后,我们对结果进行断言。
概括
我们学习了 React-testing-library,并编写了涵盖核心用例的测试。我们学习了如何处理事件、异步操作以及如何管理用户输入。我们了解了这个库的大部分功能,但更重要的是,我们学会了如何以不同的方式思考测试。
也许我们不必测试内部,而是测试组件的表面?
进一步阅读
这个库还有很多内容,鼓励你看看
继续阅读 https://dev.to/itnext/how-you-can-use-react-testing-library-to-test-component-surface-49pm