DEV Community

Matthew Palmer
Matthew Palmer

Posted on

React Unit Testing (Tutorial)

Introduction

Let's face it -- There is no better way to test your applications better than putting them to, well, the test! We can build all day long and visually get the results we are looking for. But, what happens when there is a hidden bug? What if this bug reveals a pretty serious vulnerability? All of this can be avoided in our React apps using unit testing.

Necessary Dependencies

Okay, so just what ARE the needed dependencies and how do we install them? Don't worry -- I'm including this valuable information right here and now. We are going to need a total of three dependencies, so let's start by installing those first:

  • npm install jest-dom
  • npm install react-test-renderer
  • npm install @testing-library/react

Even if you have any of these dependencies, it is ALWAYS a good idea to make sure your versioning is up-to-date. If your application was created using create-react-app, you are likely already set up with @testing-library/react since it is used for testing your default App.test.js file which comes with the initial setup.

How To Do Your Own Testing

I don't wanna be the guy to put you to work, but it's very important for you to follow along in code as you read. Unit testing isn't difficult, but it can be a little confusing and overwhelming if you try to understand it based on reading alone.

Okay, so let's get right down to it!

Application Setup (for testing)

Start by creating a new folder under src called components. Within this folder, create another folder named buttons. From here, create two files in your buttons folder. They are button.js and button.css.

Inside of button.js, place the following code:

// /src/components/buttons/button.js
import React from 'react';
import './button.css';

function Button({label}){
    return <div data-testid="button" className="button-style">{label}</div>
}

export default Button;
Enter fullscreen mode Exit fullscreen mode

Here, we are using a functional component that is taking {label} as a prop. You'll also notice we are using data-testid="button". data-* is an HTML attribute we can use for testing, and this is particularly useful for when another dev comes along and changes the name of your IDs or classes. You can look up data for more information but for those on limited time, this is a great source that sums up the concept.

Okay, so let's visit roughly the top level of our application (App.js). Apply this code:

// App.js
import React from 'react';
import Button from './components/buttons/button';

function App() {
  return (
    <div className="App">
      <header>
        <Button label="click me please"></Button>
      </header>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

The div with the class "App" isn't important, but at this point, you should delete App.test.js as editing App.js will beak the testing later. We do not need App.test.js for this tutorial.

Next, we will head back to our buttons folder and open button.css. Place in the following code:

// /src/components/buttons/button.css

.button-style {
    border: 1px solid grey;
    padding: 10px;
    text-align: center;
}
Enter fullscreen mode Exit fullscreen mode

This part about adding CSS styling isn't really necessary unless you plan on starting the application to get a visual of your rendered functional component. It was only included in the tutorial for fun! :)

Unit Testing Time

Finally, the fun part! In your src/components/buttons folder, create a new folder named __test__. Inside of this folder, we are going to create a file named button.test.js. When your unit testing begins, it will travel down the tree of your application looking for any files with .test.js as the extension. This information will be important and further explained shortly.

Inside of button.test.js, we want to start with some basic imports at the top of our file. It should look like this:

// /src/components/buttons/__test__/button.test.js

import React from 'react';
import ReactDOM from 'react-dom';
import Button from './../button';
import { render } from '@testing-library/react';
Enter fullscreen mode Exit fullscreen mode

Please check React docs and Google if you are not familiar with imports within your application tree. If you're developing in React, you should already know how to use them.

Alright, so we've taken { render } from @testing-library/react. We are immediately going to use this in our first test below our imports.

// uses @testing-library/react
it('renders without crashing',  () => {
    const div = document.createElement("div");
    ReactDOM.render(<Button></Button>, div)
})
Enter fullscreen mode Exit fullscreen mode

it() takes two arguments. We are giving the test a description string for the first argument to "renders without crashing", and then an anonymous function for the second argument which will be responsible for returning a boolean if the function executes without issue. To put it in English, we are setting a variable div assigned to document.createElement("div"). Then, we are rendering our Button component to the DOM.

To run this first test, go ahead and type npm test in your IDE terminal and press Enter when prompted. Go ahead, I'll wait. :)

...

Your first test has passed! We have validated that an element can be rendered without crashing the application. Well done! To close out your testing, just press CTRL + C in your IDE terminal. Now we can move on to unit testing for the present values in our DOM elements.

We are going to need another import. Go ahead and add this to your imports at the top:

// /src/components/buttons/__test__/button.test.js

import React from 'react';
import ReactDOM from 'react-dom';
import Button from './../button';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect'; 
// New import ^
Enter fullscreen mode Exit fullscreen mode

In @testing-library/jest-dom/extend-expect, we are gaining access to the expect() function that comes with the jest-dom dependency.

Next, add this test below your first one:

//uses @testing0library/jest-dom/extend-expect
it('renders button correctly', () => {
    const { getByTestId } = render(<Button label="click me please"></Button>)
    expect(getByTestId('button')).toHaveTextContent("click me please")
})
Enter fullscreen mode Exit fullscreen mode

This is where we are putting our Button component to the test! We are creating a button under the variable getByTestId and a label of "click me please" and expecting it to contain "click me please". This test would fail if we could not pass our props down into our component. However! Go ahead and run your tests again. Surprise! They passed again!

We have a problem though... Take a look at these two tests below:

//uses @testing0library/jest-dom/extend-expect
it('renders button correctly', () => {
    const { getByTestId } = render(<Button label="click me please"></Button>)
    expect(getByTestId('button')).toHaveTextContent("click me please")
})

it('renders button correctly', () => {
    const { getByTestId } = render(<Button label="save"></Button>)
    expect(getByTestId('button')).toHaveTextContent("save")
})
Enter fullscreen mode Exit fullscreen mode

Notice anything strange? They are both the same test, but with different prop values being tested between them.
if we try to run our tests, we get an error! found multiple elements by [data-testid="button"]. What we need to do is include some way to clean up our tests when we're done with each one. Luckily, this is very easy and simple to do.

Remember our import friend at the top import { render } from '@testing-library/react';? We're going to make one small change:

import { render, cleanup } from '@testing-library/react';.

Then, right below your imports and above your tests, include this line:

afterEach(cleanup)

Now you can run your tests again. Check it out, they're passing again!

For one final lesson, we're going to be introduced to JSON snapshots of our tests. These are useful as snapshots create an instance of our passing tests and compares that snapshot to future tests to make sure they match.

Start with adding our final import at the top of our file:
import renderer from 'react-test-renderer';

Now that we have renderer, we're going to write our final test. Place this last test at the bottom of your other tests:

// uses renderer to create a snapshot of the Button component
it('matches snapshot', () => {
    // creates snapshot and converts to JSON
    const tree = renderer.create(<Button label="save"></Button>).toJSON()
    // expects the snapshot to match the saved snapshot code found in the __snapshot__ folder
    expect(tree).toMatchSnapshot()
})
Enter fullscreen mode Exit fullscreen mode

As mentioned above, we have created a snapshot of our tests that will be used to compare to other test runs. You can find this new snapshot under /src/components/buttons/__test__/__snapshots__/button.test.js.snap. This new folder/file is created for you after you run your tests.

Conclusion

There you have it! These are the basics in React Unit Testing. Once you've got these fundamental concepts down, you can continue to explore more complex testing. Additionally, you can use tests to create labs for anyone you end up mentoring in your longterm career.

If you'd like access to the repo for this tutorial, you can find it here

Happy coding!

Top comments (0)