You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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"}]}}}'

解决建议

  1. 等待异步操作完成:sendUnsubscribe是异步函数,但handleSubscribeUnsubscribe调用时未等待其完成。Jest默认不会自动等待异步操作,导致断言执行时unsubscribeAsync可能还未触发。可以用waitFor包裹断言:

    await waitFor(() => {
      expect(mockUnsubscribeAsync).toHaveBeenCalledTimes(2);
    });
    
  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" } });
    
  3. 重置mock函数状态:在测试前清空mock调用记录,避免其他测试干扰:

    beforeEach(() => {
      mockSubscribeAsync.mockClear();
      mockUnsubscribeAsync.mockClear();
    });
    
  4. 检查useEffect清理逻辑:当前useEffect的清理函数仅更新prevSubscribeDataRef,未触发unsubscribe。如果业务上需要在sheet变化时先清理旧订阅,需在清理函数中调用sendUnsubscribe,确保旧订阅被正确取消。

内容的提问来源于stack exchange,提问作者miuosh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 13:30:18