skip to Main Content

I’m using a custom react hook to perform two GET operations using react query. The API is in a separate module and uses both getTestByUid() and getTestStatuses() inside useQuery.

// 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
  }
}

My tests are defined as follows:

// 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",
    ]);
  });
});

The first test passes fine but in the second test I get undefined returned when trying to assert testStatuses value. Why might this be happening when I am spying on the getTestStatusesByUid api call?

2

Answers


  1. You should mock getTestStatusesByUid and getTestByUid for each test case, otherwise, the test case is not isolated.

    These two test cases test the return value of the useTest hook, so there is no need to separate them. One test case is enough.

    Besides, the mock resolved values for both APIs are not correct based on the expected value in assertion statement. Since useQuery returns a data field to hold the resolved value, wrap the resolved value into { data: "mock resolved value" } is not necessary.

    E.g.

    api.js:

    export const getTestByUid = (uid) => { }
    export const getTestStatusesByUid = (uid) => { }
    

    test-hook.js:

    import { useQuery } from "react-query"
    import { getTestByUid, getTestStatusesByUid } from "./api";
    
    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 './test-hook';
    import * as testApi from './api';
    import React from 'react';
    
    jest.mock('./api');
    
    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 () => {
        testApi.getTestByUid.mockResolvedValue({ name: 'secret test' });
        testApi.getTestStatusesByUid.mockResolvedValue(['in_progress', 'ready_for_approval', 'rejected']);
    
        const { result, waitForNextUpdate } = renderHook(() => useTest('bb450409-d778-4d57-a4b8-70fcfe2087bd'), {
          wrapper,
        });
    
        await waitForNextUpdate();
        expect(result.current.test).toEqual({ name: 'secret test' });
        expect(result.current.testStatuses).toEqual(['in_progress', 'ready_for_approval', 'rejected']);
      });
    });
    

    Test result:

     PASS  stackoverflow/76369126/test-hook.test.jsx (25.099 s)
      useTestHook
        ✓ should return a test (35 ms)
    
    --------------|---------|----------|---------|---------|-------------------
    File          | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
    --------------|---------|----------|---------|---------|-------------------
    All files     |     100 |      100 |      60 |     100 |                   
     api.js       |     100 |      100 |       0 |     100 |                   
     test-hook.js |     100 |      100 |     100 |     100 |                   
    --------------|---------|----------|---------|---------|-------------------
    Test Suites: 1 passed, 1 total
    Tests:       1 passed, 1 total
    Snapshots:   0 total
    Time:        26.197 s
    

    package versions:

    "react-query": "^3.34.7",
    "react": "^16.14.0",
    "@testing-library/react-hooks": "^8.0.1",
    
    Login or Signup to reply.
  2. In your second test, you are mocking the getTestStatusesByUid function using jest.spyOn, but it seems like you are not returning the mocked value correctly. The mockImplementationOnce function expects a function that returns the mocked value, but you are passing an object instead.

    To fix the issue, you need to wrap the mocked value in a function. Here’s an updated version of your second 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).toEqual([
        "in_progress",
        "ready_for_approval",
        "rejected",
      ]);
    });

    In the updated code, the mockImplementationOnce function receives an arrow function that returns the mocked value. Also, note that I changed the expectation from toBe to toEqual since you are comparing arrays, and toEqual performs a deep comparison.

    With these changes, the second test should now correctly assert the value of testStatuses in your custom hook.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search