A Practical Guide to Testing React Applications (Unit Tests)
Tags: react, jest, typescript, testing
Published: 2024-02-26 13:45:59

### **Introduction**

Test Driven Development (TDD) is a beneficial practice for engineers, but in reality, it's not always feasible.

However, the initial investment in writing automated tests is worth the effort as it saves you time in the long run.

In this article, we’ll explore how to set up automated tests for a simple React application.

The principles used here will prove relevant for more complex scenarios.

### **How to go about testing**

You can test for many things when building, but it is important to prioritize the things with the potential to impact users the most first.

With this in mind, these questions can serve as a guide

* What is the typical workflow for users on the application?
    
* What features will cause the most disruption for the users in the event of failure?
    

Your priority should be to test for functionality rather than [implementation details](https://kentcdodds.com/blog/testing-implementation-details). In a React application, implementation details can refer to the inner workings of a function or component.

Tests should be deterministic, such that given the same inputs they always return the same results.

### **Implementation**

{% embed https://codesandbox.io/embed/testing-react-applications-r2er1t?fontsize=14&hidenavigation=1&theme=dark %}

Our demo [application](https://codesandbox.io/s/testing-react-applications-r2er1t?file=/src/App.tsx) displays a list view of users fetched from an API, a button to toggle more details for each user, and the ability to search for a specific user.

We will explore how to test using [Jest](https://jestjs.io/) and [React Testing Library](https://testing-library.com/).

**Installation**

Run the following command to install the necessary packages

```powershell
npm install --save-dev @testing-library/dom @testing-library/jest-dom @testing-library/react react-test-renderer @testing-library/react-hooks
or 
yarn add --dev @testing-library/dom @testing-library/jest-dom @testing-library/react react-test-renderer @testing-library/react-hooks
```

**GridItem.tsx**

```typescript
import { IUser, SelectedUser } from "../../models";

import styles from "./gridItem.module.css";

export interface IGridItem {
 user: IUser;
 handleOpen: () => void;
 setSelectedUser: (user: SelectedUser) => void;
}

function GridItem({ user, handleOpen, setSelectedUser }: IGridItem) {
 function handleClick() {
   setSelectedUser(user);
   handleOpen();
 }

 return (
   <article className={styles.container} data-testid="GridItem">
     <p>{user.name}</p>
     <button onClick={handleClick}>View User</button>
   </article>
 );
}

export default GridItem;
```

As we’ve established earlier we don’t test for implementation details, we only want to concern ourselves with the input and output from this component.

Here inputs are the following props:

* user object
    
* `handleOpen` function,
    
* `setSelectedUser` function to set the selected user.
    

The outputs are

* User name text
    
* a button labeled `View User` that triggers a `handleOpen` function.
    

So we test that given said input, we get the same output.

```typescript
//GridItem.test.tsx
import { render, fireEvent, screen } from "@testing-library/react";
import GridItem, { IGridItem } from "../../../components/GridItem";

const componentProps: IGridItem = {
 user: {
   avatarUrl: "https://i.pravatar.cc/150?img=68",
   id: 0,
   name: "John Smith",
   gender: "male",
   age: 20
 },
 handleOpen: jest.fn(),
 setSelectedUser: jest.fn()
};

test("Should render the username and the handleOpen function is called when 'view user' button is clicked ", () => {
 render(<GridItem {...componentProps} />);

 expect(screen.getByText(componentProps.user.name)).toBeInTheDocument();

 const viewButton = screen.getByText(/view user/i);

 fireEvent.click(viewButton);

 expect(componentProps.handleOpen).toHaveBeenCalled();
});
```

First, we define the props to be passed into the component as `componentProps`

We define our test using the [test](https://jestjs.io/docs/api#testname-fn-timeout) function from Jest. This function takes 2 parameters, a description first and a callback function containing the test logic.

We use the [render](https://testing-library.com/docs/react-testing-library/api#render), [fireEvent](https://testing-library.com/docs/dom-testing-library/api-events/#fireevent), and [screen](https://testing-library.com/docs/queries/about/#screen) utilities from [@testing-library/react](https://testing-library.com/docs/react-testing-library/intro/).

The [render](https://testing-library.com/docs/react-testing-library/api#render) utility function mounts the component into the [DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction).

The [`expect`](https://jestjs.io/docs/expect) utility from Jest gives us access to methods called [matchers](https://jestjs.io/docs/expect#matchers) we can use to check for test conditions.

We confirm the DOM contains the username we provided using the [toBeInTheDocument](https://testing-library.com/docs/guide-disappearance/) matcher from jest.

You can negate this check by using the [not](https://jestjs.io/docs/expect#not) modifier from Jest

```typescript
expect(screen.getByText(componentProps.user.name)).not.toBeInTheDocument();
```

Next, we check for a `view user` button by doing a regex case-insensitive search for the label.

We confirm that clicking the ‘View User’ button triggers the Jest [mock](https://jestjs.io/docs/mock-function-api) function assigned to ‘handleOpen’.

**Grid.tsx**

```typescript
import React from "react";
import GridItem from "../GridItem";
import { IUser, SelectedUser } from "../../models";
import useDisclosure from "../../hooks/useDisclosure";
import Modal from "../Modal";
import UserDisplay from "../UserDisplay";

import styles from "./grid.module.css";

interface IGrid {
 users: IUser[] | undefined;
}

function Grid({ users }: IGrid) {
 const { isOpen, onOpen, onClose } = useDisclosure();
 const [selectedUser, setSelectedUser] = React.useState<SelectedUser>(
   undefined
 );

 function handleCloseModal() {
   onClose();
   setSelectedUser(undefined);
 }

 if (!users || !users.length)
   return <p className={styles.no_result}>No user found</p>;

 return (
   <>
     {isOpen ? (
       <Modal title="User Info" onClose={handleCloseModal} isOpen={isOpen}>
         <UserDisplay user={selectedUser} />
       </Modal>
     ) : null}

     <div className={styles.container}>
       {users.map((user) => (
         <GridItem
           user={user}
           key={user.id}
           handleOpen={onOpen}
           setSelectedUser={setSelectedUser}
         />
       ))}
     </div>
   </>
 );
}

export default Grid;
```

Here we want to assert that given a list of users, the component returns;

* A list of users (`GridItem` component) equal to the number of items on the list
    
* A descriptive error when no user is provided.
    

```typescript
//Grid.test.tsx
import Grid from "../../../components/Grid";
import { IUser } from "../../../models";
import { render, screen } from "@testing-library/react";

const users: IUser[] = [
 {
   avatarUrl: "https://i.pravatar.cc/150?img=68",
   id: 0,
   name: "John Smith",
   gender: "male",
   age: 20
 },
 {
   avatarUrl: "https://i.pravatar.cc/150?img=48",
   id: 1,
   name: "Martha Liberty",
   gender: "female",
   age: 18
 }
];

test("It renders the list of user", () => {
 render(<Grid users={users} />);

 expect(screen.getAllByTestId("GridItem")).toHaveLength(users.length);
});

test("it shows descriptive text when no user is provided", () => {
 render(<Grid users={undefined} />);

 expect(screen.getByText(/No user found/i)).toBeInTheDocument();
});
```

First, [render](https://testing-library.com/docs/react-testing-library/api#render) the `Grid` component with an array of users as the ‘user’ props value

Select all the instances of `GridItem` rendered using the  [`testId`](https://testing-library.com/docs/queries/bytestid/) attribute with [getAllByTestId](https://testing-library.com/docs/queries/bytestid/).

Assert that the occurrence of `GridItem` in the DOM is equal to the number of `users` supplied.

`data-testid` is a special attribute used for testing, and should be used sparingly after the other queries don't work for your use case.

We also confirm that `No user found` is displayed when the `users` prop is `undefined`.

**UserDisplay.tsx**

```typescript
import { SelectedUser } from "../../models";

import styles from "./userDisplay.module.css";

function UserDisplay({ user }: { user: SelectedUser }) {
 if (!user) return null;

 return (
   <article className={styles.container}>
     {user.avatarUrl && (
       <img src={user.avatarUrl} alt={user.name} data-testid="UserImage" />
     )}

     <div>
       <h1>{user.name}</h1>
       <p>{user.gender}</p>
       <p>{user.age}</p>
     </div>
   </article>
 );
}

export default UserDisplay;
```

To test this component, we assert that given the right props, it renders the user details including user including the user’s image, name, gender, and age.

```typescript
//UserDisplay.test.tsx
import UserDisplay from "../../../components/UserDisplay";
import { axe } from "jest-axe";
import { render, screen } from "@testing-library/react";
import { IUser } from "../../../models";

const user: IUser = {
 avatarUrl: "https://i.pravatar.cc/150?img=68",
 id: 0,
 name: "John Smith",
 gender: "male",
 age: 20
};

test("Should render the user name, gender and age", () => {
 render(<UserDisplay user={user} />);

 expect(screen.getByText(user.name)).toBeInTheDocument();
 expect(screen.getByText(user.gender)).toBeInTheDocument();
 expect(screen.getByText(user.age)).toBeInTheDocument();
 expect(screen.getByTestId("UserImage")).toBeInTheDocument();
});
```

First, we [Render](https://testing-library.com/docs/react-testing-library/api#render) the `UserDisplay` component with a sample user object.

Then we write assertions for the properties of the user object by using corresponding matchers.

We can also write tests to verify that this component does not have accessibility violations (e.g. alt attribute for the user image is present).

We use the j[est-axe](https://www.npmjs.com/package/jest-axe) package, which extends jest with custom matchers from [axe-core](https://github.com/dequelabs/axe-core), an accessibility testing engine for HTML-based user interfaces.

**Installation**

```powershell
yarn add jest-axe @types/jest-axe 
or 
npm i jest-axe @types/jest-axe
```

`jest-axe` has a utility `toHaveNoViolations` which asserts that a React component does not have any accessibility violations according to the Axe rules.  
In `setupTests.ts` import `toHaveNoViolations` and extend jest using the [extend](https://jestjs.io/docs/expect#expectextendmatchers) utility.

```typescript
//setupTests.ts
import { toHaveNoViolations } from "jest-axe";
expect.extend(toHaveNoViolations);
```

To test for accessibility violations we define an additional test function in the `UserDisplay.test.tsx` file.

```typescript
//UserDisplay.test.tsx
test("should not fail any accessibility tests", async () => {
 const { container } = render(<UserDisplay user={user} />);

 expect(await axe(container)).toHaveNoViolations();
});
```

`Container` here is a variable that refers to the root DOM element that contains the rendered output from the render method.

We check for accessibility compliance by calling the `toHaveNoViolations` matcher on the container variable.

**Modal.tsx**

```typescript
import React from "react";

import styles from "./modal.module.css";

function disablePageScroll() {
 document.body.style.overflow = "hidden";
}

function enablePageScroll() {
 document.body.style.overflow = "unset";
}

interface IModal {
 title: string;
 onClose: () => void;
 isOpen: boolean;
 children: React.ReactNode;
}

function Modal(props: IModal) {
 const { title, onClose, isOpen, children } = props;

 React.useEffect(() => {
   if (isOpen) {
     disablePageScroll();
   }
   return () => {
     enablePageScroll();
   };
 }, [isOpen]);

 return (
   <div role="dialog" aria-modal="true">
     <div
       data-testid="modalOverlay"
       className={styles.modal_overlay}
       onClick={onClose}
     ></div>

     <div className={styles.modal_wrapper}>
       <p className={styles.modal_title}>{title}</p>
       <div className={styles.modal_content}>{children}</div>
       <button onClick={onClose}>Close modal</button>
     </div>
   </div>
 );
}

export default Modal;
```

The component takes the following props;

* a title, `isOpen` boolean,
    
* onClose function triggered when the modal overlay or `Close modal` button is clicked
    
* `children` property for rendering nested items, Here we use the text `React is fun`
    

```typescript
//Modal.test.tsx
import Modal from "../../../components/Modal";
import { render, fireEvent } from "@testing-library/react";

const componentProps = {
 title: "Random Title",
 onClose: jest.fn(),
 isOpen: true,
 children: <p>React is fun</p>
};

const renderComponent = (props = componentProps) =>
 render(<Modal {...props} />);

test("Modal should render title, children and close button ", () => {
 const { getByText } = renderComponent();

 expect(getByText(componentProps.title)).toBeInTheDocument();

 expect(getByText(/React is fun/i)).toBeInTheDocument();

 expect(getByText(/Close modal/i)).toBeInTheDocument();
});

test("clicking the close button or the modal overlay should call the onClose function", () => {
 const { getByText, getByTestId } = renderComponent();
 const closeButton = getByText(/Close modal/i);
 const modalOverlay = getByTestId("modalOverlay");

 fireEvent.click(closeButton);
 fireEvent.click(modalOverlay);

 expect(componentProps.onClose).toHaveBeenCalledTimes(2);
});
```

First, we check for the title in the DOM.

Next, check for `children` by searching for the text “React is fun” and a button with a `close modal` label is rendered.

Next, we check that the `onClose` function is called by either clicking the `close modal` button or the modal overlay.

We do this by using the [toHaveBeenCalledTimes](https://jestjs.io/docs/expect#tohavebeencalledtimesnumber) matcher from jest which takes in the number of times we expect a mock function to be called.

### **Testing React hooks**

UseDisclosure.tsx

```typescript
import React from "react";

const defaultState = { isOpen: false };

const actions = {
 ON: "ON",
 OFF: "OFF",
 TOGGLE: "TOGGLE"
} as const;

type reducerType = { type: keyof typeof actions };

function disclosureReducer(state: typeof defaultState, action: reducerType) {
 switch (action.type) {
   case actions.ON:
     return { isOpen: true };
   case actions.OFF:
     return { isOpen: false };
   case actions.TOGGLE:
     return { isOpen: !state.isOpen };

   default:
     return state;
 }
}

function useDisclosure(initialState = defaultState) {
 const [{ isOpen }, dispatch] = React.useReducer(
   disclosureReducer,
   initialState
 );

 const onOpen = () => dispatch({ type: actions.ON });
 const onClose = () => dispatch({ type: actions.OFF });
 const onToggle = () => dispatch({ type: actions.TOGGLE });

 return { isOpen, onOpen, onClose, onToggle };
}

export default useDisclosure;
```

This is a simple hook that allows you to handle common state open, close, and toggle scenarios.  
[@testing-library/react-hooks](https://react-hooks-testing-library.com/) exposes a `renderHook` function that can render the custom hook, and it returns a `result` property on which we can run assertions.

```typescript
//UseDisclosure.test.tsx
import useDisclosure from "../../../hooks/useDisclosure";
import { renderHook, act } from "@testing-library/react-hooks";

test("returns the correct initial values", () => {
 const { result } = renderHook(() => useDisclosure({ isOpen: true }));

 expect(result.current.isOpen).toBe(true);
 expect(typeof result.current.onOpen).toBe("function");
 expect(typeof result.current.onClose).toBe("function");
 expect(typeof result.current.onToggle).toBe("function");
});

test("toggles the state correctly", () => {
 const { result } = renderHook(() => useDisclosure());

 // Check that the initial state is false
 expect(result.current.isOpen).toBe(false);

 act(() => result.current.onOpen()); // open fn
 expect(result.current.isOpen).toBe(true);

 act(() => result.current.onClose()); // Close fn
 expect(result.current.isOpen).toBe(false);

 act(() => result.current.onToggle()); // Toggle fn
 expect(result.current.isOpen).toBe(true);

 act(() => result.current.onToggle());
 expect(result.current.isOpen).toBe(false);
});
```

To check that the hook returns the right values, we render the hook with an initial value of `true`, then write assertions to confirm that `isOpen` evaluates to `true` and `onOpen`, `onClose`, and `onToggle` are functions.

Next, we test that the isOpen property returns false when an initial value is absent.

[@testing-library/react](https://react-hooks-testing-library.com/)[\-ho](https://react-hooks-testing-library.com/reference/api#act)[oks](https://react-hooks-testing-library.com/) exposes a method called [act](https://react-hooks-testing-library.com/reference/api#act) that allows us to run updates to component state synchronously.

Using this method we can simulate the action of calling the `onOpen`, `onClose`, and `onToggle` functions and check that the `isOpen` value updates accordingly.

Finally to run the test use `yarn test` or `npm test`.

You can add the `--watch` flag to run this command in watch mode so the tests will automatically re-run whenever there are changes to the test file.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1708679436019/815f22e0-7942-4d2c-b2a2-87130b381ec7.png align="center")

### **Conclusion**

There are several other things we can explore with testing, like snapshot testing, and checking for false positives, but these are beyond the scope of this post.

You can find the full implementation in this [Codesandbox](https://codesandbox.io/s/testing-react-applications-r2er1t)

In the next one, we will look at how to incorporate integration tests in the same application.

Cover photo by [Rahul Mishra](https://unsplash.com/@rahuulmiishra?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash) on [Unsplash](https://unsplash.com/photos/a-computer-monitor-sitting-on-top-of-a-desk-7vNPUkY3gg4?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash)