Ant Design v4 与 React Testing Library 组件测试:Select & AutoComplete 解决方案
2025-01-23 21:37:24
解决 Ant Design v4 与 React Testing Library 在 Select 和 Autocomplete 组件上的测试问题
Ant Design v4 引入了一些架构上的变动,这些变动会影响到使用 React Testing Library 进行组件测试的方式。 特别是在 Select
和 AutoComplete
组件上,开发者可能会遇到点击后下拉选项不出现,导致测试失败的情况。这并非代码本身的问题,而是因为 React Testing Library 在默认情况下可能无法完全捕获到 Ant Design v4 这些组件动态渲染的行为。 本文将针对这一问题分析原因并提供解决方案。
问题分析
问题的根本原因在于 Ant Design v4 对于某些组件(比如 Select
, AutoComplete
, Tooltip
等)的下拉菜单或者弹窗采用的是 portal 的机制渲染,而非直接渲染在当前 DOM 树中。 而 React Testing Library 的 JSDOM 模拟环境默认不跟踪这种 portal 渲染行为,所以导致测试中我们无法查找到期望出现的下拉菜单 DOM 结构。也就是说,虽然我们在测试代码中模拟了点击事件,组件也的确执行了打开下拉的操作,但在 JSDOM 虚拟环境中并没有新的 DOM 元素生成并加入到我们所监控的测试范围内。
解决方案
针对上述问题,以下提供几种常见的解决方案。
1. waitFor
等待机制
最直接的方式是在点击操作之后使用 waitFor
API 进行异步等待,让组件有足够时间完成 portal 元素的渲染。 waitFor
函数会循环检测我们设定的期望元素是否存在于文档中,当超时或找到元素时,返回对应的 Promise 状态。这种方案最通用也易于理解。
代码示例:
import "@testing-library/jest-dom/extend-expect";
import React from "react";
import { render, fireEvent, waitFor, getByText } from "@testing-library/react";
import App from "./App";
test("App Test", async () => {
const { queryAllByText, getByText, container } = render(<App />);
expect(queryAllByText("Lucy").length).toBe(1);
expect(queryAllByText("Jack").length).toBe(0);
fireEvent.click(getByText("Lucy"));
// 必须使用 await 等待 waitFor 执行完成,因为他是异步的
await waitFor(() => {
expect(queryAllByText("Jack").length).toBe(1);
});
});
步骤:
- 安装
@testing-library/react
。 - 使用
render
渲染你的组件。 - 使用
fireEvent.click
触发Select
组件的点击事件,使其下拉列表展开。 - 使用
waitFor
函数来等待目标下拉选项(例如 "Jack")的出现。 请注意,必须使用async/await
保证waitFor
函数执行完毕。 - 使用
expect
进行断言。
这种方式保证了代码在目标选项渲染完成之后才进行断言,从而解决下拉菜单 DOM 未渲染导致的测试失败。
2. 指定 container
参数查找 portal
元素
React Testing Library 中的 render 函数可以接受一个container
参数,这允许我们将组件渲染到一个指定的 DOM 元素中。默认情况下 React Testing Library 创建的 container
位于 document.body
的外部, 如果某些组件创建 portal 默认使用 document.body
作为锚点,这时我们可以利用该参数来指定 render 到一个已经挂载在 document.body
上的 div 中,然后通过 document.body
查询相关的元素。
代码示例:
import "@testing-library/jest-dom/extend-expect";
import React from "react";
import { render, fireEvent, screen } from "@testing-library/react";
import App from "./App";
test("App Test with document.body container", () => {
const div = document.createElement('div')
document.body.appendChild(div);
const { queryAllByText, getByText } = render(<App />, { container: div });
expect(queryAllByText("Lucy").length).toBe(1);
expect(queryAllByText("Jack").length).toBe(0);
fireEvent.click(getByText("Lucy"));
// 直接通过 screen 查询 body 下的元素
expect(screen.queryAllByText("Jack").length).toBe(1);
document.body.removeChild(div); // Cleanup
});
步骤:
- 在
document.body
中创建div
元素。 - 将 div 传递给
render
作为container
参数。 - 使用
screen.queryAllByText
或screen.getByText
查询portal
元素,而不是通过container
实例查询。 - 组件测试完成之后将 div 从
document.body
移除。
此方案利用了 ReactDOM 的渲染特性,确保 portal 元素与测试代码处于同一作用域内。
3. 针对 Select
组件特定的测试方案
一些情况下 waitFor
依然无法满足需要,可能是因为 antd 的下拉元素有比较复杂的动态生成逻辑,并且会在某个状态发生之后才存在于页面,这种时候可以通过直接通过模拟 onChange
事件进行断言来避免操作 UI。
代码示例
import "@testing-library/jest-dom/extend-expect";
import React from "react";
import { render, getByText } from "@testing-library/react";
import App from "./App";
test("App Test - programmatically select value", () => {
const { container } = render(<App />);
const selectElement = container.querySelector('.ant-select-selector');
// 检查默认选中的是 lucy
expect(getByText(container, 'Lucy')).toBeInTheDocument()
const selectValue= 'jack';
// 使用 ant design 自定义的 select event 来模拟选项选择
fireEvent.mouseDown(selectElement, { target: { className: 'ant-select-selector' } });
const optionElement = getByText(container, 'Jack');
fireEvent.click(optionElement);
// 检查组件状态
expect(getByText(container, 'Jack')).toBeInTheDocument();
});
步骤:
- 使用
querySelector
选取 Antd Select 组件。 - 使用
fireEvent.mouseDown
+fireEvent.click
触发 Select 选项选中逻辑。 - 使用
getByText
直接验证选项的值已经生效。
此方案规避了复杂的 UI 交互,更加关注组件内部状态和逻辑,是一种更加轻便高效的测试方案。
安全建议
- 避免使用硬编码的
timeout
。waitFor
和waitForNextUpdate
会更适合这种异步操作的断言。 - 对于复杂的组件交互,先进行小的测试单元,缩小错误范围,然后再构建更完整的测试用例。
- 测试应当覆盖组件的关键功能,关注用户视角。
- 合理使用
debug
工具排查渲染和事件问题。
通过以上分析,在升级Ant Design v4时遇到React Testing Library 测试问题的开发者可以根据自己的实际情况选择合适的解决方案,确保组件的功能正常以及测试顺利运行。