TkDodo/testing-react-query

React Query Test with MSW - renderHook result.current always returns null

rostgoat opened this issue · 1 comments

I am trying to test my react query hook using msw but renderHook's result's current value is always null. Why?

Note: the reason fetch does not include a URL is because this is a Rails Propshaft app and React and Rails apps share the same port.

In the real app, everything works and the hook returns data just fine but not in the test.

followed your original tutorial

reproducible sandbox

App.tsx

const App = () => {
	const {
		isLoading,
		data,
		error
	} = useFetchAccounts();

	if (isLoading) return "Loading...";

	if (error) return "An error has occurred: " + error;

	return (
		<div>
			{data?.map(item => (
				<div key={item.id}>{item.value}</div>
			))}
		</div>
	);
};

App.spec.tsx

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

testQueryClient.setQueryDefaults(["accounts"], { retry: 5 });

describe("Accounts", () => {
  it("renders accounts", async () => {
    const { result } = renderHook(() => useFetchAccounts(), {
      wrapper: () => (
        <QueryClientProvider client={testQueryClient}>
          <App />
        </QueryClientProvider>
      )
    });

    console.log("result", result);
    await waitFor(() => expect(result.current.isSuccess).toBe(true));
  });
});

hooks.tsx

import { useQuery } from "@tanstack/react-query";

type FormattedAccount = {
  id: number;
  label: string;
};

const fetchAccount = async () => {
  const res = await fetch("/api/accounts");
  const accounts = await res.json();

  console.log("accounts", accounts);
  const formattedAccounts: FormattedAccount[] = [...accounts]?.map(
    (option) => ({
      id: option.id,
      label: option.name,
    })
  );

  console.log("formattedAccounts", formattedAccounts);
  return formattedAccounts;
};

export const useFetchAccounts = () => {
  return useQuery({
    queryKey: ["accounts"],
    queryFn: () => fetchAccount()
  });
};

server.ts

import { setupServer } from "msw/node";
import { rest } from "msw";

export const handlers = [
  rest.get("/api/accounts", (req, res, ctx) => {
    const rres = res(ctx.status(200), ctx.json([{ id: 1, name: "Account 1" }]));
    console.log("rres", rres);
    return rres;
  })
];

export const server = setupServer(...handlers);

jest.config.js

/** @type {import('ts-jest').JestConfigWithTsJest} */

module.exports = {
  preset: "ts-jest",
  collectCoverage: true,
  collectCoverageFrom: ["src/**/*.spec.{ts, tsx}"],
  coverageDirectory: "coverage",
  testEnvironment: "jsdom",
  setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
  transform: {
    "^.+\\.(ts|tsx)?$": "ts-jest"
  }
};

jest.setup.ts

import "@testing-library/jest-dom";
import "whatwg-fetch";
import { server } from "./server";

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

export default global.matchMedia =
  global.matchMedia ||
  function (query) {
    return {
      matches: false,
      media: query,
      onchange: null,
      addListener: jest.fn(), // deprecated
      removeListener: jest.fn(), // deprecated
      addEventListener: jest.fn(),
      removeEventListener: jest.fn(),
      dispatchEvent: jest.fn()
    };
  };
TkDodo commented

you're mixing things here a bit. You either want to test a component, or the hook.

If you test a component, test a real component (like App.tsx), and as a wrapper, pass in the QueryClient only. Then, your assertions should check for html output:

describe('query component', () => {
test('successful query component', async () => {
const result = renderWithClient(<Example />)
expect(await result.findByText(/mocked-react-query/i)).toBeInTheDocument()
})

if you want to test the hook, use renderHook, but then you don't need a DummyComponent - just the hook with the Provider as well:

https://github.com/TkDodo/testing-react-query/blob/20e35ed4180cca6a6942b6984cfd57474c4be3d7/src/tests/hooks.test.tsx#L9C1-L17


btw, result.current is null because wrapper is a react component that gets children passed, and you need to render them, which you don't:

    const { result } = renderHook(() => useFetchAccounts(), {
-      wrapper: () => (
+     wrapper: ({ children }) => (
        <QueryClientProvider client={testQueryClient}>
-          <DummyComponent />
+          {children}
        </QueryClientProvider>
      )
    });

this basically gives you the "testing the hook" solution, but I'd prefer testing a complete component instead.