Writing unit tests with React, Typescript, and react-testing-library
Tags: typescript, react, javascript, reacttestinglibrary
Published: 2020-02-23 20:39:31

---
title: Writing unit tests with React, Typescript, and react-testing-library
published: true
description: On how to utilize Typescript features while writing tests for React components written in Typescript.
tags: #Typescript, #React, #JavaScript, #react-testing-library
cover_image: https://miro.medium.com/max/1024/1*Ukhx76VQ8E6JXEW7xfIzSA.png
---

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`.

```tsx
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:

```tsx
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.

```tsx
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).

```tsx
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.
```tsx
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:

```tsx
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.

```tsx
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.


```tsx
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:
```ts
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.

```tsx
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`.

```tsx
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.

```tsx
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](https://kentcdodds.com/blog/avoid-nesting-when-youre-testing?fbclid=IwAR2snxztWxnzUQGzIG42DrmqedSrzmmdfYgBWva7_Vzb3bgj6QR8UazP5_4) 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](https://twitter.com/wm_matuszewski)

Thanks 👋