Snippets I Always Need to Look Up When Writing Tests with React Testing Library

jamenamcinteer

Jamena McInteer

Posted on April 15, 2020

Snippets I Always Need to Look Up When Writing Tests with React Testing Library

Whenever I start a new project and need to write React Testing Library (RTL) unit tests, I end up having to look up or copy snippets of code to set up the tests. I thought it might be useful to share some of these here in one place, both for myself and for others who might want to look up these snippets quickly. Feel free to leave a comment if any of these don't work for you or if there is a better way to do something. I will update this post as I gather more snippets over time.

Taking a snapshot

I normally don't use snapshots in my unit tests as they are more trouble than they're worth, but sometimes when debugging a unit test that is failing I want to see what the unit test is seeing, so I'll throw in a snapshot temporarily. I can never remember the exact code for taking a snapshot, and since I don't use them I can't refer to them in another part of the codebase. So here it is:

it("renders", () => {
  const { asFragment } = render(<MyComponent />);
  expect(asFragment()).toMatchSnapshot();
});

Mocking Axios API Calls

This is something I've had to learn how to do from several different sources as most articles will provide the basics using get for example, and skip over how to mock post requests. Or they will assume one call is being made instead of potentially many calls. Here's what I have figured out so far.

Setup

import axios from "axios";
...
jest.mock("axios");
const mockedAxios = axios as jest.Mocked<typeof axios>;

CancelToken

I use CancelToken to cancel axios API calls when components unmount. The above code is modified to the following to support CancelToken:

import axios from "axios";
...
jest.mock("axios");
const mockedAxios = axios as jest.Mocked<typeof axios>;
type Canceler = (message?: string) => void;

class CancelToken {
  public static source() {
    const cancel: Canceler = jest.fn();
    const token = new CancelToken();
    return {
      cancel,
      token
    };
  }
}

// @ts-ignore
mockedAxios.CancelToken = CancelToken;

Note that this works for me but does throw TypeScript errors as its not fully built out.

Get

it("renders", () => {
  const response1 = {
    data: [], // populate with mock data as desired
    status: 200
  }
  const response2 = {
    data: [], // populate with mock data as desired
    status: 200
  }
  mockedAxios.get.mockImplementation(url => {
    if (url === "/api/somePath") return Promise.resolve(response1);
    if (url === "/api/someOtherPath") return Promise.resolve(response2);
    // etc.
  });
  const { getByText } = render(<MyComponent />);
  ...
});

Post

it("renders", () => {
  const response1 = {
    data: [], // populate with mock data as desired
    status: 200
  }
  const response2 = {
    data: [], // populate with mock data as desired
    status: 200
  }
  mockedAxios.post.mockImplementation((url, payload) => {
    if (url === "/api/somePath" && payload.someProperty === "some value") {
      return Promise.resolve(response1);
    }
    if (url === "/api/somePath" && payload.someProperty === "some other value") {
      return Promise.resolve(response2);
    }
    // etc.
  });
  const { getByText } = render(<MyComponent />);
  ...
});

Render with Theme (Styled Components)

I will normally create a render-methods.tsx file and add the different render methods I need and import them into my tests. One of these is rendering with the Styled Components theme.

import React from "react";
import { render } from "@testing-library/react";
import { ThemeProvider } from "styled-components";

import theme from "../../theme"; // or wherever your theme file is

export function renderWithTheme(component: React.ReactNode) {
  return render(<ThemeProvider theme={theme}>{component}</ThemeProvider>);
}

Render with Router (Reach Router) and Mocking navigate

In my render-methods.tsx file, I have a renderWithRouter method that also incorporates rendering with a Styled Components theme. If you're not using Styled Components or a theme, this portion can be removed of course (see the second code snippet).

import React from "react";
import { render } from "@testing-library/react";
import { ThemeProvider } from "styled-components";
import {
  createHistory,
  createMemorySource,
  LocationProvider
} from "@reach/router";

import theme from "../../theme"; // or wherever your theme file is

export function renderWithRouter(
  component: React.ReactNode,
  { route = "/", history = createHistory(createMemorySource(route)) } = {}
) {
  return {
    ...render(
      <ThemeProvider theme={theme}>
        <LocationProvider history={history}>{component}</LocationProvider>
      </ThemeProvider>
    ),
    history
  };
}

Without theme:

import React from "react";
import { render } from "@testing-library/react";
import {
  createHistory,
  createMemorySource,
  LocationProvider
} from "@reach/router";

export function renderWithRouter(
  component: React.ReactNode,
  { route = "/", history = createHistory(createMemorySource(route)) } = {}
) {
  return {
    ...render(
      <LocationProvider history={history}>{component}</LocationProvider>
    ),
    history
  };
}

In my test file, I mock navigate so it works properly and can be tested.

import React from "react";
import { fireEvent, wait } from "@testing-library/react";
import { navigate } from "@reach/router";

jest.mock("@reach/router", () => {
  const RouterMocks = jest.requireActual("@reach/router");
  return {
    ...RouterMocks,
    navigate: jest.fn()
  };
});

it("navigates", () => {
  const { getByText } = render(<MyComponent />);
  fireEvent.click(getByText("Submit"));
  await wait(() => {
    expect(navigate).toHaveBeenCalledTimes(1);
    expect(navigate).toHaveBeenCalledWith("/pathOfNextPage");
  });
});

If you need to mock parameters in the route, you can add that to the jest mock as well:

jest.mock("@reach/router", () => {
  const RouterMocks = jest.requireActual("@reach/router");
  return {
    ...RouterMocks,
    navigate: jest.fn(),
    useParams: () => ({
      someParameter: 'some-parameter-value'
    })
  };
});

Dealing with Reach UI styles not found warning

If you're using Reach UI for components like Tooltip or Dialog, you may get a warning about styles not found. This is a work-around to get that warning to go away (this warning does not show up in the browser, just in unit tests).

const { getComputedStyle } = window;
beforeAll(() => {
  window.getComputedStyle = jest.fn().mockReturnValue({
    getPropertyValue: jest.fn().mockReturnValue("1")
  });
});
afterAll(() => {
  window.getComputedStyle = getComputedStyle;
});

Clicks with User Event

Since some components may use onMouseDown instead of onClick, using RTL's fireEvent to simulate clicks may not work. I use the user-event package instead which handles this case.

import userEvent from "@testing-library/user-event";
...
it("clicks", () => {
  const { getByText } = render(<MyComponent />);
  userEvent.click(getByText("Submit"));
});

Stubbing Date.now()

Sometimes you'll want to stub Date.now() to a fixed time when making time comparisons. There are a few different ways of doing this, but this is a simple method that I use in my tests.

it("stubs a date", () => {
  const realDateNow = Date.now.bind(global.Date);
  const dateNowStub = jest.fn(() => 1577838600);
  global.Date.now = dateNowStub;

  // Write the unit test as usual

  global.Date.now = realDateNow; // reset the global.Date.now when you are done

Handling TypeScript error regarding property 'value'

When using TypeScript with React Testing Library, I get the following error when trying to assert on an input field's value: Property 'value' does not exist on type 'HTMLElement'. This can be remedied by casting to HTMLInputElement. It's not particularly pretty having this all over your tests, but it works.

For example, this would throw the TypeScript error:

expect(getByLabelText("Name").value).toBe("Lilliana");

This can be updated to the following to fix the TypeScript error:

expect((getByLabelText("Name") as HTMLInputElement).value).toBe("Lilliana");
💖 💪 🙅 🚩
jamenamcinteer
Jamena McInteer

Posted on April 15, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related