NextJS 13中React useEffect在Jest测试中无限循环问题
问题
我用NextJS 13写了一个Members组件,里面的useEffect依赖searchParams触发数据获取。用Jest测试时,mock了next/navigation的useSearchParams,但测试超时,看起来是useEffect无限循环了,怀疑是mock的get方法导致的,该怎么修改这个mock?
组件代码
export default function Members() { // States const [users, setUsers] = useState<MemberType[]>([]); const [loading, setLoading] = useState(false); const searchParams = useSearchParams(); // To refresh the page const router = useRouter(); const inputRef = useRef<HTMLInputElement | null>(null); // Function to fetch a book const fetchBooks = async (userQuery: string | null) => { if (!userQuery) { const res = await fetch("/api/users/members", { method: "GET", headers: { "Content-Type": "application/json" }, }); const body: MemberType[] = await res.json(); return body; } const res = await fetch(`/api/users/members/${userQuery}`, { method: "GET", headers: { "Content-Type": "application/json" }, }); const body: MemberType[] = await res.json(); return body; }; // Runs if the search parameters change useEffect(() => { setLoading(true); const searched = searchParams.get("search"); console.log(searched); fetchBooks(searched).then((users) => { setUsers(users); setLoading(false); }); if (searched && inputRef.current) { inputRef.current.value = searched; } }, [searchParams]); //...
测试文件中的mock
jest.mock("next/navigation", () => ({ useRouter: () => ({ push: jest.fn(), }), useSearchParams: () => ({ get: jest.fn().mockReturnValue(''), }), }));
测试代码
describe("Members page", () => { it("should render the page correctly", async () => { await act(async () => { render(<Members />); }); }); });
解决方案
问题根源是你mock的useSearchParams每次调用都会返回新的对象实例,而React的useEffect依赖对比是引用级别的——组件每次渲染时,searchParams都是全新的对象,导致useEffect反复触发,形成无限循环。
具体修复步骤:
让searchParams返回稳定的引用
把mock的searchParams对象提取到mock函数外部,确保每次调用useSearchParams都返回同一个实例:// 定义稳定的searchParams实例,放在mock外部 const mockSearchParams = { get: jest.fn().mockReturnValue(''), }; jest.mock("next/navigation", () => ({ useRouter: () => ({ push: jest.fn(), }), // 每次调用都返回同一个对象 useSearchParams: () => mockSearchParams, }));mock fetch请求避免异常
组件里的fetchBooks会发起真实网络请求,测试时不mock的话可能导致超时或失败,全局mock fetch:// 在测试文件顶部添加fetch mock global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve([]), // 模拟接口返回空用户列表 ok: true, }) ); // 测试后清除mock,避免用例间污染 afterEach(() => { jest.clearAllMocks(); });简化测试代码
React Testing Library的render已经自动处理了act的异步逻辑,不需要手动包裹:describe("Members page", () => { it("should render the page correctly", async () => { render(<Members />); // 可以添加断言验证组件状态,比如等待loading结束 await waitFor(() => { expect(screen.queryByText('加载中')).not.toBeInTheDocument(); }); }); });
扩展测试场景
如果要测试不同搜索参数的情况,可以在单个用例中修改mock的返回值:
it("should fetch filtered users when search param exists", async () => { // 修改mock返回特定搜索值 mockSearchParams.get.mockReturnValue('test-user'); render(<Members />); // 断言fetch调用了正确的接口地址 await waitFor(() => { expect(global.fetch).toHaveBeenCalledWith('/api/users/members/test-user', expect.any(Object)); }); });
内容的提问来源于stack exchange,提问作者Danny Yuan
相关产品推荐
相关产品推荐

