Testing modern ReactJS Application: Unit Testing(Part 2)
Tags: javascript, webdev, react, testing
Published: 2022-03-18 22:24:58

As I promised in previous post in this part I would like to tell about unit testing of components, difference between testing functional and class components and how to test react hooks

## Table of contents
1. [Class vs. Function](#Class vs. Function)
2. [Testing React Components](#Testing React Components)
    1. [Experiment Subjects](#Experiment Subjects)
    2. [Rendering Result](#Rendering Result)
    3. [Asynchronous Rendering Result](#Asynchronous Rendering Result)
3. [Hook testing](#Hook testing)

## Class vs. Function

As you may know ReactJS offers two ways of writing components: class-based and function-based. Latter approach offered more concise way of writing components and in the meantime enabled usage of [React Hooks](https://reactjs.org/docs/hooks-intro.html)

In terms of testing there is significant difference between classes and functions. Functions, defined inside of function components cannot be mocked. If for some reason you want to have possibility to mock any of methods used in your component consider using class-based approach

In my opinion, this limitation is not limitation at all since React components represent some parts of User Interface and therefore should not be tested the same way we test backend code. You'll get what I mean a bit later

## Testing React Components

### Experiment Subjects

Before writing any tests we need few components to test. In the beginning of each section I will provide content of component I'm going to test. You are free to use any other component for experimenting

### Rendering Result

Component to test:

```javascript
import React from 'react';

export default function TestComponent({ children }) {
	return (
		<div>
			{ children }
		</div>
	);
};
```

When it comes to testing rendering output we can't rely on snapshots since they meant for regression testing

> Regression testing is a software testing practice that ensures an application still functions as expected after any code changes, updates, or improvements

When we need to ensure that component renders properly on given input(or without it) `@testing-library/react` steps in

Firstly, we will need to render component. For this to manage we need to import `render` function from `@testing-library/react`. Component will be rendered in artificial DOM. To easily find nodes in this DOM we will import `screen` object

```javascript
import { render, screen } from '@testing-library/react';
```

Next we use this `render` function to render component the way we need. Simply pass JSX as argument

```javascript
render(
            <TestComponent>
                <div>Node</div>
            </TestComponent>
        );
```

Now we can use queries provided by `testing-library` in `screen` object. As React components are about building User Interface that is presented to end user those queries provide methods to find nodes the way users see them. It becomes more clear when you see it in action

Now we expect to see node with text `Node`. Let's literally try to find such element. We can do it following way

```javascript
screen.findByText('Node')
```

And to actually check that we found it, let's expect that resulted value is defined

```javascript
expect(screen.getByText('Node')).toBeDefined();
```

If we run script `test` we should see successful output. In case you need to update snapshots don't forget to add `-u` flag

![first yarn test result testing rendering result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ijwnn9aqv48ft2bvny3x.png)
 
But currently our new test suite is not self-descriptive and informative. Library `@testing-library/jest-dom` provide many additional matchers for DOM nodes. Import it into test file

```javascript
import '@testing-library/jest-dom';
```

And now replace matcher `toBeDefined` with the one called `toBeInTheDocument`

```javascript
expect(screen.getByText('Node')).toBeInTheDocument();
```

Rerun script `test` and check if testing passes

![script test result with matcher toBeInTheDocument](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jlqbkzt6rlg0xcx0r096.png)

### Asynchronous Rendering Result

It's very common when component performs request to API and waits for response before rendering final result. Firstly, adjust TestComponent to mock server request and append conditional rendering

```javascript
import React, { useEffect, useState } from 'react';

export default function TestComponent({ children }) {
	const [isLoading, setIsLoading] = useState(true);

	useEffect(() => setTimeout(() => setIsLoading(false), 100), []);

	if (isLoading) {
		return (
			<div>Loading</div>
		);
	}

	return (
		<div>
			{ children }
		</div>
	);
};
```

I'll use `useState` and `useEffect` hooks combined with setTimeout to postpone state change

Now since our component renders result not instantly, different query should be used. Queries provided by `testing-library` that allow to work with asynchronous rendering start with `find` prefix instead of `get` we used previously for synchronously rendered content

Important notice: `find` queries waits up to 1000ms

Make test suite's callback `async`, replace query with `findByText` and await on returned Promised. Looks like following

```javascript
it('should render properly', async () => {
        render(
            <TestComponent>
                <div>Node</div>
            </TestComponent>
        );

        expect(await screen.findByText('Node'))
            .toBeInTheDocument();
    });
```

Script `test` result:

![async rendering test result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6v2y6o729l6014anlket.png)
 
Now let's also make sure that `Loading` node is rendered initially. Simply use query `getByText` to look for node containing `Loading` text before last `expect` were we await until final result is rendered

```javascript
expect(screen.getByText('Loading'))
            .toBeInTheDocument();

expect(await screen.findByText('Node'))
            .toBeInTheDocument();
```

Script `test` result:

![second async rendering test result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3no2hk54x3prq2mpindi.png)

[More info on queries provided by `testing-library`](https://testing-library.com/docs/queries/about)
 
## Hook testing

I'll write simple hook that mocks request to server the same way I did it previously using setTimeout to add artificial delay

```javascript
export function useMockRequest(delay) {
	const [isLoading, setIsLoading] = useState(true);

	useEffect(() => setTimeout(() => setIsLoading(false), delay), []);

	return [isLoading];
}
```

TestComponent file:

```javascript
import React, { useEffect, useState } from 'react';

export function useMockRequest(delay) {
	const [isLoading, setIsLoading] = useState(true);

	useEffect(() => setTimeout(() => setIsLoading(false), delay), []);

	return [isLoading];
}

export default function TestComponent({ children }) {
	const [isLoading] = useMockRequest(100);

	if (isLoading) {
		return (
			<div>Loading</div>
		);
	}

	return (
		<div>
			{ children }
		</div>
	);
};
```

Firstly, let's add new `describe` block to our test file and give it a title `useMockRequest`. Create one test inside of new `describe` for further usage. I will name it `should change state after given delay`

> In real world application you would create new file for new test block but currently I want to keep it simple

Now let's clarify what are React Hooks.

React Hooks are functions that enable control on your component's behavior. When it comes to testing it can be a bit misleading since you cannot really use capabilities of React hooks outside of a component. Or can we?

`testing-library` provides one more library exactly for such purpose allowing us to avoid headache and safe strength for actual testing. It is called [`@testing-library/react-hooks`](https://react-hooks-testing-library.com)

Let's add it as development dependency

npm:

```
npm install -D @testing-library/react-hooks
```

yarn:

```
yarn add -D @testing-library/react-hooks
```

It provides a lot of tools for easy and comfortable hook testing but let's check them step-by-step

First thing that needs to be done is hook rendering. Our new library will do all hard work itself. See how it looks like below

```javascript
import { renderHook } from '@testing-library/react-hooks';

...

const result = renderHook(() => useMockRequest(100));
```

To render hook we need `renderHook`. Pretty straightforward, isn't it?

Then you call it and pass callback as argument inside of which you invoke your hook with or without arguments

The result of invocation is an object that provides many fields and utility functions to proceed with rendered hook testing

First thing we need to get is the actual result of hook invocation since we have to verify that initial state is equal to `true`. Hook's return value can be accessed by `result` field of an object returned by `renderHook` function. I will utilise destructuring to keep code concise

```javascript
const { result } = renderHook(() => useMockRequest(100));
```

Object `result` also contains multiple fields but we should be interested in `current` as it is containing exactly what we need

Since our hook returns array of two elements, `current` property will be exactly this array. To validate that state, returned by hook initially is false, just access first element of `current` property and add assertion on that

```javascript
expect(result.current[0])
            .toBeTruthy();
```

First state is tested, next thing that should be checked is that state changes after some time and to achieve that we need to wait for hook to rerender. Exactly for this purpose `renderHook` returns function called `waitForNextUpdate`

To wait for next hook update we have to... `await` a Promise this function returns

```javascript
await waitForNextUpdate();
```

> Be aware that by default it waits up to 1000ms but it can be changed by passing additional argument

Once promise had been awaited, we can check absolutely the same value to be changed - the one inside of `result.current[0]`. Now we expect it to be `false`

```javascript
expect(result.current[0])
            .toBeFalsy();
```

Full test case looks like that

```javascript
const { result, waitForNextUpdate } = renderHook(() => useMockRequest(100));

expect(result.current[0])
    .toBeTruthy();

await waitForNextUpdate();

expect(result.current[0])
    .toBeFalsy();
```

Run script `test` to invoke all tests

![result of yarn test after adding test for hook](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/biim7fppy3rc772ujifz.png)
 
This was only top of the mountain in regards of testing hooks. I'll dive deeply into this topic in separate post or series of posts

Here is the [GitHub repo](https://github.com/Mr0Bread/react-testing-tutorial-app) with all my code in one place if you need

In addition, feel free to leave a comment about what did you like and what did you not

In regards of this post this is it for today. See you next time!