renderHook重渲染后mock调用断言异常问题求助
问题:renderHook测试自定义Hook时,unsubscribeAsync调用次数断言失败
我在用renderHook测试包含依赖[sheet]的useEffect的自定义Hook时遇到问题:修改sheet对象后调用rerender(也试过rerender({sheet})),控制台显示unsubscribeAsync函数执行了2次,但Jest断言提示仅执行1次。
测试代码
import { renderHook } from "@testing-library/react"; import { DataHubService } from "./DataHubService"; import { useCurrentDataSubscription } from "./useCurrentDataSubscription.hook"; import { renderHookWrapper } from "./utils/renderHookWrapper"; const mockSubscribeAsync = jest .fn() .mockImplementation(() => Promise.resolve()); const mockUnsubscribeAsync = jest .fn() .mockImplementation(() => Promise.resolve()); jest.mock("./DataHubService", () => { return { DataHubService: jest.fn().mockImplementation(() => { return { connection: { on: jest.fn() }, subscribeAsync: mockSubscribeAsync, unsubscribeAsync: mockUnsubscribeAsync }; }) }; }); describe("useCurrentDataSubscription", () => { test("should call unsubscribe when sheet change", () => { // Arrange const mockHubConnection = new DataHubService("test"); let sheet: any = { Id: 1, Name: "Test" }; // Act const { rerender } = renderHook( () => useCurrentDataSubscription(mockHubConnection, sheet, []), { wrapper: renderHookWrapper } ); sheet = { Id: 2, Name: "new Test sheet" }; rerender(); // Assert expect(mockUnsubscribeAsync).toHaveBeenCalledTimes(2); }); });
自定义Hook代码(useCurrentDataSubscription.hook.ts)
import { useCallback, useEffect, useRef } from "react"; import { subscribe, unsubscribe } from "./redux/features/currentData-slice"; import { useAppDispatch } from "./redux/hooks"; export function useCurrentDataSubscription( hubConnection: any, sheet: any, alarm: any[] ) { const dispatch = useAppDispatch(); const currentSubscribeDataRef = useRef<any>({ values: {} }); const prevSubscribeDataRef = useRef<any>({ values: {} }); const sendSubscribe = useCallback( async (subscribeData: any) => { try { console.group(">>> Subscribe"); console.dir(JSON.stringify(subscribeData)); console.groupEnd(); dispatch(subscribe(subscribeData)); await hubConnection?.subscribeAsync(subscribeData); } catch (error) {} }, [dispatch, hubConnection] ); const sendUnsubscribe = useCallback( async (unsubscribeData: any) => { try { console.group(">>> UnSubscribe"); console.dir(JSON.stringify(unsubscribeData)); console.groupEnd(); dispatch(unsubscribe(unsubscribeData)); await hubConnection?.unsubscribeAsync(unsubscribeData); } catch (error) {} }, [dispatch, hubConnection] ); const handleSubscribeUnsubscribe = useCallback(() => { // 实际业务中会根据currentSubscribeDataRef和prevSubscribeDataRef计算参数,此处简化调用 sendSubscribe({ values: {} }); sendUnsubscribe({ value: {} }); }, [sendSubscribe, sendUnsubscribe]); const getSubscribeData = useCallback( (sheet: any, alarm: any) => { // 解析sheet对象返回数据,逻辑不影响问题 return { values: {} }; }, [sheet, alarm] ); useEffect(() => { currentSubscribeDataRef.current = getSubscribeData(sheet, alarm); handleSubscribeUnsubscribe(); return () => { prevSubscribeDataRef.current = getSubscribeData(sheet, alarm); }; }, [sheet, alarm, getSubscribeData, handleSubscribeUnsubscribe]); return { subscribeData: currentSubscribeDataRef.current, prevSubscribeData: prevSubscribeDataRef.current }; }
运行日志
> > > > >>> SubscribeAsync console.dir > > '{"values":{"TWSRV":{"variables":[{"name":"variable1","mode":1}],"subscription":64},"TWSRV2":{"variables":[{"name":"variable1","mode":1},{"name":"variable2","mode":1}],"subscription":64}}}' > > > > console.group > > >>> UnSubscribeAsync > > console.dir > > '{"values":{}}' > > // sheet change - call rerender > > > > console.group > > >>> SubscribeAsync > > console.dir > > '{"values":{}}' > > > > console.group > > >>> UnSubscribeAsync > > console.dir > > '{"values":{"TWSRV":{"variables":[{"name":"variable1"}]},"TWSRV2":{"variables":[{"name":"variable1"},{"name":"variable2"}]}}}'
解决建议
等待异步操作完成:
sendUnsubscribe是异步函数,但handleSubscribeUnsubscribe调用时未等待其完成。Jest默认不会自动等待异步操作,导致断言执行时unsubscribeAsync可能还未触发。可以用waitFor包裹断言:await waitFor(() => { expect(mockUnsubscribeAsync).toHaveBeenCalledTimes(2); });修正rerender的参数传递:当前
rerender()未传递更新后的sheet,由于闭包特性,Hook无法感知变量变化。需在rerender时显式传递新参数:// 初始化时使用props形式 const { rerender } = renderHook( ({ sheet }) => useCurrentDataSubscription(mockHubConnection, sheet, []), { wrapper: renderHookWrapper, initialProps: { sheet } } ); // 更新时传递新sheet rerender({ sheet: { Id: 2, Name: "new Test sheet" } });重置mock函数状态:在测试前清空mock调用记录,避免其他测试干扰:
beforeEach(() => { mockSubscribeAsync.mockClear(); mockUnsubscribeAsync.mockClear(); });检查useEffect清理逻辑:当前
useEffect的清理函数仅更新prevSubscribeDataRef,未触发unsubscribe。如果业务上需要在sheet变化时先清理旧订阅,需在清理函数中调用sendUnsubscribe,确保旧订阅被正确取消。
内容的提问来源于stack exchange,提问作者miuosh
相关产品推荐
相关产品推荐

