Unit Testing UseReact

July 7, 2023    React Web-Development Unit Testing

Unit Testing Components that use UseReact

We’ve been learning React for our new app we are building. I’m a firm believer in having automated tests (is this a unit or integration test? :-)). Testing Library is a great place to start. We want to have tests on our components. Over the last few months, I’ve had trouble faking the useQuery Api/Http calls. I got it working today! I don’t have a lot of new info to share, but will give you the links that helped me and a small code snippet.

Don’t Mock fetch, use msw instead

Start with Kent C Dodds article Stop Mocking Fetch . That lead me to MSW .The author did a major update to get it to work with Node 18 (a big thanks to him for all his work) and now v1.2.3 is out (as of July 20, 2023).

"The release itself is stable and I don't anticipate any major changes to it. That being said, it is quite a breaking change, and I'm still working on updating our docs to help people migrate easier. I'd expect this version to land as latest in NPM sometime later this year (based on my availability). Meanwhile, you can rely on msw@next with absolute certainty." (kettanaito, 2 weeks ago, Maintainer, Author in the Github issue)

I also used the TanStack testing guide . I used this example to setup a test component, and wrote some “learning tests” to make sure it worked, then combined with Kent C Dodds’ code approach. (Be sure to check out TanStack ’s open source tools for React. They are really well made and useful.)

Kent C Dodds again helped me out with Testing React Query . That links to this repo that has examples.

My component test was failing until I added the waitFor(. Thanks to the question for pointing that out.

on July 25, 2023, I updated my happy-dom to 10.5.2 and msw to 1.2.3. I started getting a ERR_INVALID_URL error. I had to add location.href = 'https://myurl.com/' to my vitest.setup.ts. I found this from https://github.com/capricorn86/happy-dom/issues/678 and looked at this commit https://github.com/Lipoic/Lipoic-Frontend/commit/cf83276c147e19820f639b3eb34b6220263a4f27

update: November 1, 2023. MSW 2.0 is now available . My code below will work with a few modifications.

my react-network.test.utils.tsx

// https://github.dev/TkDodo/testing-react-query/blob/main/src/tests/hooks.test.tsx
// https://tkdodo.eu/blog/testing-react-query
// https://tanstack.com/query/v4/docs/react/guides/testing
// https://kentcdodds.com/blog/stop-mocking-fetch

import { render } from '@testing-library/react';
import { beforeAll, afterEach, afterAll } from 'vitest';
// msw mocks at the network level (fetch)
// 7/6/2023, needed to use `npm i msw@next` for now see https://github.com/mswjs/msw/discussions/1464#discussioncomment-6280250
// 7/23/2023 `npm i msw` works now that v1+ was released, v1.2.3 now
import { HttpResponse, RestHandler, rest } from 'msw';
import * as React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

import { SetupServer, setupServer } from 'msw/node';
import { fakeEnabledProducts } from '@features/products/product-models';
import { AuthContextProps } from 'react-oidc-context';

export const fakeAuth = {
  user: {
    access_token: 'letMeIn',
  },
} as AuthContextProps;

export const defaultOkEnabledHandlers = [
  // has to be an absolute url for node.js and test setup
  rest.get('https://myurl.com/api/enabled', (_, res, ctx) => {
    return res(ctx.json(fakeEnabledProducts));
  }),
];

export const defaultOkDisabledHandlers = [
  rest.get('https://myurl.com/api/disabled', (_, res, ctx) => {
    return res(ctx.json(fakeDisabledProducts));
  }),
];

// can add more handlers as needed

// code from Kent C Dodd's article
const createTestQueryClient = () =>
  new QueryClient({
    defaultOptions: {
      queries: {
        retry: false,
      },
    },
    logger: {
      // eslint-disable-next-line no-console
      log: console.log,
      // eslint-disable-next-line no-console
      warn: console.warn,
      // eslint-disable-next-line @typescript-eslint/no-empty-function
      error: () => {},
    },
  });

export function renderWithClient(ui: React.ReactElement) {
  const testQueryClient = createTestQueryClient();
  const { rerender, ...result } = render(
    <QueryClientProvider client={testQueryClient}>{ui}</QueryClientProvider>
  );
  return {
    ...result,
    rerender: (rerenderUi: React.ReactElement) =>
      rerender(<QueryClientProvider client={testQueryClient}>{rerenderUi}</QueryClientProvider>),
  };
}

export function createWrapper() {
  const testQueryClient = createTestQueryClient();
  // eslint-disable-next-line react/display-name
  return ({ children }: { children: React.ReactNode }) => (
    <QueryClientProvider client={testQueryClient}>{children}</QueryClientProvider>
  );
}

export function createFakeServer(handlers: RestHandler[]) {
  // server
  return setupServer(...handlers);
}
export function startFakeServer(server: SetupServer) {
  server.listen();
}
export function cleanupFakeServer(server: SetupServer) {
  server.resetHandlers();
}
export function closeFakeServer(server: SetupServer) {
  server.close();
}

export function setupFakeServerWithCleanupAfter(server: SetupServer) {
  // Establish API mocking before all tests.
  beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
  // Reset any request handlers that we may add during the tests,
  // so they don't affect other tests.
  afterEach(() => server.resetHandlers());
  // Clean up after the tests are finished.
  afterAll(() => server.close());
}
describe('test network faking', () => {
  it('fakes the useGetEnabledProducts at the network level', async () => {
    const server = createFakeServer([
      ...defaultOkEnabledHandlers,
      // add more handlers as needed...defaultOkDisabledHandlers,
    ]);
    startFakeServer(server);
    const { result } = renderHook(() => useGetEnabledProducts(1, fakeAuth), {
      wrapper: createWrapper(),
    });
    await waitFor(() => expect(result.current.isSuccess).toBe(true));
    expect(result.current.data?.length).toBe(fakeEnabledProducts.length);
    expect(result.current.data ? result.current.data[0].id : '').toBe(fakeEnabledProducts[0].id);
    cleanupFakeServer(server);
    closeFakeServer(server);
  });
});

// you could use vi.mock, but I think the MSW approach will make for better tests as Mr. Dodds says
describe('vi.mock', () => {
    it('fake api.getEnabledProducts with vi.mock', async () => {
      // here's how to fake the return value for a *.api.ts method
      const mock = vi.fn().mockImplementation(getEnabledProducts);
      mock.mockReturnValue({
        data: fakeEnabledProducts,
        isLoading: false,
        isError: false,
        isSuccess: true,
      });
      const results = await getEnabledProducts(
        'api/enabled',
        fakeAuth.user?.access_token || ''
      );
      expect(results.length).toBe(fakeEnabledProducts.length);
      expect(results[0].id).toBe(fakeEnabledProducts[0].id);
      vi.clearAllMocks();
    });
  });

A Component Test

describe('Dashboard Page', () => {
  const server = createFakeServer([
    ...defaultOkEnabledHandlers,
    //...defaultOkDisabledHandlers,
  ]);
  setupFakeServerWithCleanupAfter(server);
  // I added another learning test, duplicated from above to make sure it works here
  it('is useGetEnabledProducts faked correctly?', async () => {
    const { result } = renderHook(() => useGetEnabledProducts(1, fakeAuth), {
      wrapper: createWrapper(),
    });
    await waitFor(() => expect(result.current.isSuccess).toBe(true));
    expect(result.current.data?.length).toBe(fakeEnabledProducts.length);
    expect(result.current.data?.at(0)?.id).toBe(fakeEnabledProducts[0].id);
  });
  // you'll have to imagine what the Dashboard component looks like
  it('shows products in the grid', async () => {
    const queryClient = queryClientFactory();
    customRender(<Dashboard />, { client: queryClient } as QueryClientProviderProps);
    // waitFor the re-render after the useQuery calls are complete
    const gridContainer = await waitFor(() => screen.getByTestId('products-grid-container'));
    const gridRows = gridContainer.querySelectorAll('tr');
    expect(gridRows[0].innerText).contains('Product 1');
    expect(gridRows[1].innerText).contains('Product 2');
    expect(gridRows.length).toBe(2);
  });

I hope this helps you get your integration tests up and running in a shorter time than it took me.



Watch the Story for Good News
I gladly accept BTC Lightning Network tips at [email protected]

Please consider using Brave and adding me to your BAT payment ledger. Then you won't have to see ads! (when I get to $100 in Google Ads for a payout, I pledge to turn off ads)

Use Brave

Also check out my Resources Page for referrals that would help me.


Swan logo
Use Swan Bitcoin to onramp with low fees and automatic daily cost averaging and get $10 in BTC when you sign up.