使用 React 测试库解决维护难题
编写测试是保证软件质量的关键环节,而对于React来说, React Testing Library是首选的解决方案。但如果我们不够谨慎,测试套件可能会变成维护的噩梦。
我们来解决这个问题。
测试渲染函数
为了提高测试的可维护性,我们能做的最好的事情就是使用一个单独的函数来渲染组件并查询其元素。我们称之为测试渲染函数。
Kent C. Dodds在他的关于测试隔离的文章中提到了这种方法,它有可能改变你编写测试的方式。
举个例子:
import { render, screen } from '@testing-library/react';
import ToDoScreen from './ToDoScreen';
describe('ToDoScreen', () => {
function renderToDoScreen() {
render(<TodoScreen />);
return {
name: screen.getByLabelText('Task'),
add: screen.getByText('Add')
};
}
it('should add a task', () => {
const { name, add } = renderToDoScreen();
// ...
});
});
让我们深入探讨一下这种方法能给我们带来什么。
保持测试内容易于阅读
你有没有过这样的经历:读完一份测试题,却发现理解起来比预期花费的时间要长得多?查询逻辑增加了一层额外的代码,我们需要仔细筛选才能找到我们真正想要的东西:场景。
以下是一个将所有查询内联的示例:
it('should close the form after add', async () => {
render(<PersonScreen />);
// open the form
fireEvent.click(screen.getByText('Toggle Form'));
// fill it out
fireEvent.change(
screen.getByLabelText('Name'),
{ target: { value: "Derek" } }
);
// click add
fireEvent.click(screen.getByText('Add'));
// the form should now be closed
expect(screen.queryByLabelText('Name')).toBeNull();
});
说实话,对于这样的小规模测试来说,情况还不算太糟,但是当测试规模变大时,就很难排除干扰因素,理解测试场景。
我们改用测试渲染函数并进行比较。
it('should close the form after add', async () => {
const { toggleForm, form } = renderPersonScreen();
// open the form
fireEvent.click(toggleForm);
// fill it out
fireEvent.change(
form.name,
{ target: { value: "Derek" } }
);
// click add
fireEvent.click(form.add);
// the form should now be closed
expect(form.name).toBeNull();
});
我不知道你怎么想,但我更喜欢这种方式。阅读测试代码时,我们真的会在意按钮来自 `<div>` getByText、getByRole`<span>` 还是 `<div> ` 吗getByTestId?使用测试渲染函数可以帮助我们的测试专注于场景,而不是纠结于如何定位 UI 元素。步骤应该很清晰,其他的都只是实现细节。
可预测的测试
单独运行测试的结果应该与运行测试套件中的所有测试的结果相同。在测试期间设置全局变量可能会导致一起运行测试时失败,如果这些变量中的任何一个没有在测试中正确重置beforeEach。
测试渲染函数将每个测试独立出来,使其结果更可预测。我们来看一个例子:
describe('AsyncSelect', () => {
function renderAsyncSelect() {
const fetchOptions = jest.fn();
render(
<AsyncSelect
getOptions={fetchOptions}
{/* other props */}
/>
)
return {
fetchOptions,
openMenu: // ...
};
}
it('should call the fetch after the menu opens', () => {
const { fetchOptions, openMenu } = renderAsyncSelect();
expect(fetchOptions).not.toHaveBeenCalled();
openMenu();
expect(fetchOptions).toHaveBeenCalled();
});
it('should call the fetch on search', () => {
const { fetchOptions, openMenu } = renderAsyncSelect();
expect(fetchOptions).not.toHaveBeenCalled();
// ...
});
});
在上面的例子中,我们连续进行了两个测试,对fetchOptions模拟对象进行断言,这无需任何额外考虑即可正常工作,因为模拟对象在测试渲染函数中被重新构建。
考虑一下另一种选择:
describe('AsyncSelect', () => {
let fetchOptions = jest.fn();
function renderAsyncSelect() {
// ...
}
// ...
});
如果我们这样做,就会出现问题。模拟对象在测试之间没有重置,因此单个测试会通过,但一起运行时会失败。
这种事真让人怀疑自己的职业选择。而这一切都是因为我们忘了我们需要一个beforeEach……
let fetchOptions;
beforeEach(() => {
fetchOptions = jest.fn();
});
使用测试渲染函数可以完全消除这个问题,我们甚至不需要考虑这个问题。
集中查询
在测试中直接查询 UI 元素会导致额外的工作量,尤其是在 HTML 结构发生变化、我们使用的第三方组件更新到新版本,甚至是 React Testing Library 本身发生变化时。届时,我们将不得不逐个修复每个失败的测试用例。
如果所有查询都集中在一个测试渲染函数中,我们就只需要在一个地方纠正问题。
可重用组件
到目前为止,我们一直在讨论单个文件的测试渲染函数,但我们可以将其扩展到代码库中最可重用的组件:模态框、日期选择器、下拉列表等。
我们的大部分测试(如果不是全部的话)都会用到这类组件。如果我们决定从一个第三方下拉菜单切换到另一个,就必须更新所有测试来解决这个问题。
我们可以通过为这些组件构建测试助手来避免这种噩梦,这样,替换第三方组件只需要更新我们的测试助手即可。
概括
- 测试渲染功能帮助我们解决了维护难题。
- 抽象查询逻辑使我们的测试更易于阅读。
- 隔离测试可以提高测试结果的可预测性。
- 集中查询并为最可重用的组件编写测试辅助程序,可以使我们的测试面向未来。