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

React自定义Hook双GET查询测试:第二个useQuery返回undefined排查

问题

在自定义Hook useTest 中通过React Query执行两个GET请求(getTestByUid 和 getTestStatusesByUid),使用@testing-library/react-hooks和Jest测试时,第一个验证test数据的测试通过,但第二个验证testStatuses的测试中,该值返回undefined,尽管已对getTestStatusesByUid做了spy模拟。

相关代码

TestHook.js

import { useQuery } from "react-query" 

export const useTest = (uid) => {
  const { data: test } = useQuery("test", () => getTestByUid(uid));
  const { data: testStatuses } = useQuery("statuses", () => getTestStatusesByUid(uid));
  
  return {
    test,
    testStatuses
  }
}

test-hook.test.js

import { renderHook } from "@testing-library/react-hooks";
import { QueryClient, QueryClientProvider } from "react-query";
import { useTest } from "../src/hooks/TestHook";
import * as testApi from "../src/api/test-api.js";

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: false,
    },
  },
});

const wrapper = ({ children }) => {
  return (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
};

describe("useTestHook", () => {
  it("should return a test", async () => {
    jest
      .spyOn(testApi, "getTestByUid")
      .mockImplementationOnce(() =>
        Promise.resolve({ data: { name: "secret test" } })
      );
    const { result, waitForNextUpdate } = renderHook(
      () => useTest("bb450409-d778-4d57-a4b8-70fcfe2087bd"),
      { wrapper: wrapper }
    );
    await waitForNextUpdate();
    expect(result.current.test).toBe({ name: "secret test" });
  });

  it("should return statuses for a test", async () => {
    jest.spyOn(testApi, "getTestStatusesByUid").mockImplementationOnce(() =>
      Promise.resolve({
        data: ["in_progress", "ready_for_approval", "rejected"],
      })
    );
    const { result, waitForNextUpdate } = renderHook(
      () => useTest("bb450409-d778-4d57-a4b8-70fcfe2087bd"),
      { wrapper: wrapper }
    );
    await waitForNextUpdate();
    expect(result.current.testStatuses).toBe([
      "in_progress",
      "ready_for_approval",
      "rejected",
    ]);
  });
});

原因及解决方案

核心原因

  1. 未模拟全部API请求:第二个测试只模拟了getTestStatusesByUid,但useTest会同时触发getTestByUid请求,未被模拟的请求会处于pending状态,导致组件无法更新到testStatuses返回的状态。
  2. waitForNextUpdate的局限性:该方法仅等待一次状态更新,当Hook中有两个并行请求时,第一次更新可能只是其中一个请求完成,此时testStatuses仍为undefined。
  3. 断言方式错误:用toBe比较对象/数组时,是严格引用比较,而返回的新对象/数组和测试字面量不是同一引用,即使数据正确也会断言失败(这也是潜在问题)。

解决步骤

1. 每个测试模拟所有用到的API

确保Hook中调用的所有API都被模拟,避免请求挂起:

it("should return statuses for a test", async () => {
  // 同时模拟两个API方法
  jest.spyOn(testApi, "getTestByUid").mockImplementationOnce(() =>
    Promise.resolve({ data: { name: "secret test" } })
  );
  jest.spyOn(testApi, "getTestStatusesByUid").mockImplementationOnce(() =>
    Promise.resolve({
      data: ["in_progress", "ready_for_approval", "rejected"],
    })
  );
  const { result } = renderHook(
    () => useTest("bb450409-d778-4d57-a4b8-70fcfe2087bd"),
    { wrapper: wrapper }
  );
});

2. 用waitFor替代waitForNextUpdate

waitFor会持续等待直到断言通过,适合多并行请求场景:

await waitFor(() => {
  expect(result.current.test).not.toBeUndefined();
  expect(result.current.testStatuses).not.toBeUndefined();
});

3. 用toEqual替代toBe比较对象/数组

toEqual递归比较值,而非引用,适合对象和数组的断言:

// 替换原有的toBe断言
expect(result.current.test).toEqual({ name: "secret test" });
expect(result.current.testStatuses).toEqual([
  "in_progress",
  "ready_for_approval",
  "rejected",
]);

4. 测试后清理模拟和QueryClient

避免测试间污染,在afterEach中添加清理逻辑:

afterEach(() => {
  jest.clearAllMocks();
  queryClient.clear();
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 14:53:22