Writing unit tests with React, Typescript, and react-testing-library

Wojciech Matuszewski - Feb 23 '20 - - Dev Community

The company I work for started embracing Typescript as a go-to solution for writing React. During code reviews, I noticed a lot of people had problems while testing their components. While looking at the code, I noticed that it was written in such a way that made Typescript look more like a burden and not a tool that assists you while writing code.

Having some experience with Typescript I came up with a pattern for writing tests which, in my opinion, avoids unnecessary repetition and makes them clear.

Example Component

This is the component we are going to test. It is quite simple but contains enough logic so that we can use a couple of features of jest and react-testing-library.

import React from "react";
import { Todo } from "./Todo";

type Props = {
  id: number;
  onClick: (todo: Todo) => void;
};

type State = {
  fetchState: "loading" | "error" | "success";
  todo: Todo | undefined;
};

function Todo({ id, onClick }: Props) {
  const [state, setState] = React.useState<State>({
    fetchState: "loading",
    todo: undefined
  });

  React.useEffect(() => {
    function fetchTodo() {
      fetch(`https://jsonplaceholder.typicode.com/todos/${id}`)
        .then<Todo>(response => response.json())
         // Normally we would probably check if the component 
         // is still mounted here, before using `setState`
        .then(todo => setState({ todo, fetchState: "success" }))
        .catch(() => setState({ todo: undefined, fetchState: "error" }));
    }
    fetchTodo();
  }, [id]);

  if (state.fetchState == "loading" || !state.todo) return <p>loading ...</p>;
  if (state.fetchState == "error") return <p>error!...</p>;

  return (
    <div onClick={() => onClick(state.todo as Todo)}>
      <p>{state.todo.title}</p>
      <p>{state.todo.id}</p>
    </div>
  );
}

Like I said the code here does not really matter. It's just here so that we have something to test.

Tests

Your test cases would probably look like this:

import { render } from "@testing-library/react";
it("fetches a todo", () => {
  const {/* selectors */} = render(<Todo onClick={() => {}} id={1} />);
  // rest of the test
});

it("handles non-existing id", () => {
  const {/* selectors */} = render(<Todo onClick={() => {}} id={420} />);
  // rest of the test
});

// more test cases

And there is nothing wrong with that.

But when writing fourth, fifth test case you may get tired of all this repetition. Notice that I had to explicitly provide onClick function even though that function will not be used within the test (eg. handles non-existing id)?

We can remove all of this repetition by creating renderUI or setup function (these are just propositions, call it what you want).

renderUI function

Let's create renderUI function which will be responsible for rendering the component and returning react-testing-library selectors and utilities.

function renderUI(props: ?) {
  return render(<Todo {...props}/>)
}

Now, I left the question mark here on purpose. You might be tempted to just import the type of props from ./App (the file that holds the component we are testing).

import { render } from "@testing-library/react";
import { Todo, Props } from "./App";

function renderUI(props: Props) {
  return render(<Todo {...props} />);
}

While you certainly can do that, I personally do not recommend doing so.

  • unless you use verbose names like TodoComponentProps, exporting the type of component props may cause collisions with other exported types, this can be especially painful when using code completion.

  • exporting the type of component props can be confusing for the future reader of the code. Can I change the name of the type?, Are those used somewhere?.

With that in mind, lets leverage Typescript features and get the type of component props without exporting/importing them.

import { render } from "@testing-library/react";
import { Todo } from "./App";

type ComponentProps = React.ComponentProps<typeof Todo>;

function renderUI(props: ComponentProps) {
  return render(<Todo {...props} />);
}

I'm using generic React.ComponentProps defined within @types/react to get the type I need. No exporting/importing of the props type needed!

With that, within our test, we got rid of some repetition:

it("fetches a todo", () => {
  const { /* selectors */ } = renderUI({ onClick: () => {}, id: 1 });
  // rest of the test
});

But still, we have to include properties that are not really important for a given test case (onClick in this case). Parial<T> from Typescript utility types can help with that.

import { Todo } from "./App";

type ComponentProps = React.ComponentProps<typeof Todo>;

const baseProps: ComponentProps = {
  onClick: () => {},
  id: 1
};

function renderUI(props: Partial<ComponentProps> = {}) {
  return render(<Todo {...baseProps} {...props} />);
}

Notice that I had to create baseProps. These should be specified in such a manner that your component can actually render using them. The baseProps and props combo allows us to only pass these properties to renderUI function which matters in the context of a given test.

it("handles non-existing id", () => {
  const {/* selectors */} = render(<Todo id={420} />);
  // rest of the test
});

The handles non-existing id test case does test the ability to respond to user clicks so it does not specify onClick function. This is possible because we included baseProps within our renderUI function.

Rerendering

Sometimes, you need to use the rerender function returned from react-testing-library render function to test how the component behaves when given prop changes (before and after the change).

Looking at the signature of the rerender function:

rerender: (ui: React.ReactElement) => void;

it takes an parameter of type React.ReactElement. This means that our renderUI function, as it stands, will not cut it.

it("reacts to id change", () => {
  const { rerender } = renderUI({ id: 1 });
  // assert
  rerender(<Todo {...baseProps} id={2} />);
  // assert
});

We can abstract the rerender function in the same way we abstracted render.

function renderUI(props: Partial<ComponentProps> = {}) {
  const rtlProps = render(<Todo {...baseProps} {...props} />);

  return {
    ...rtlProps,
    rerender: (newProps: Partial<ComponentProps>) =>
      rtlProps.rerender(<Todo {...baseProps} {...props} {...newProps} />)
  };
}

I've replaced the returned rerender function. Instead of returning the original one, it now abstracts the renedring of the component away, which makes our tests clearer.

it("reacts to id change", () => {
  const { rerender } = renderUI({ id: 1 });
  // assert
  rerender({ id: 2 });
  // assert
});

Word of caution

I just want to point out that, sometimes, repetition is not necessarily a bad thing. Creating hasty abstractions surely is worse than having to pass props multiple times.

This is why I only recommend following the advice I'm giving here if and only if you feel the need to do so.

There is a great article which you definitely should read and consider before creating any kind of abstractions within your tests (and in general).

Summary

Overall, I think this pattern can help you write tests faster and with less repetition.

Please keep in mind that I'm no expert in the field of testing and/or Typescript so if something feels off or incorrect to you, please reach out!

You can follow me on twitter: @wm_matuszewski

Thanks 👋

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .