Last time I explained a little about testing concepts and basic testing. As a React developer primarily, I tend to test things that are in React. So let's take a React-specific focus on testing, using tools that are the simplest ones to reach for. If you use React but have never bothered with testing, this might be useful to get started with minimal fuss.
In this post we will look at:
- React Testing Library
- Unit Tests with Data Test Ids
- Interactive Tests with FireEvent
- Clean up
- Integration Testing with a little gotcha.
Introduction to React Testing Library
To be able to test React code life is much easier with React Testing Library to allow us to properly query whats going on with React to build our tests. The other popular dog in this world is Enzyme. Which is better is a debate for an internet search. But React Testing Library has more of a focus on the DOM and what the user actually sees whereas Enzyme focuses on the component itself. Remember that for later...
If you are using create-react-app then the good news is that React Testing Library is built-in, otherwise, we can add it with:
npm install --save-dev @testing-library/react
Quick note: For the sake of clarity and brevity, I'll be breezing over the step by step TDD approach, namely:
- RED: Start with the simplest test that proves something is missing.
- GREEN: Write the simplest way to make the test pass.
- Refactor, improve the code till you are happy with it
But hopefully, you can see where those steps would exist in the process.
Unit Tests with Data Test IDs
Let's pretend we want to have a component called Greeter whose job it is to show a div that says 'Howdy'. In the test file, we can provide assertions using a bunch of queries made available to us via React Testing Library (and DOM testing Library which is merged into it).
import React from 'react'
import { render } from 'react-testing-library';
import Greeter from './Greeter';
test('<Greeter/>', () => {
const {debug, getByTestId}= render(< Greeter/>);
debug(); //outputs the dom to see what it is, useful for building tests so handy for building the test.
expect(getByTestId('greeter-heading').tagName).toBe('div');
expect(getByTestId('example-heading').textContent).toBe('Howdy');
})
So what's this getByTestId business? Data Test IDs let us identify elements so we can see what's going on there. We can assign a test id by simply adding the id in our JSX we write to pass the test:
import React, { Component } from 'react'
export default class Greeter extends Component {
state = {
greeting: "Howdy" //Let's assume it is in the state because it might change
}
render() {
const { greeting } = this.state
return (
<div data-testid='greeter-heading'>
{ greeting }
</div>
)
}
}
Of course, we don't have to use data test ids. To get a fuller taste of what you can query look at the cheatsheets for React Testing Library and DOM Testing Library. It should cover everything you might want to query so I don't have to!
Building More Interactive Tests
React is all about interactions so we need to test that the interface actually works by testing the interactivity of React.
For this let's dream up a component that is a counter that ticks up every time we click the button. Let's jump to the point where we have a test and js file that is not yet interactive, in other words, a dumb button that says 0:
//Test File
import React from 'react'
import { render} from 'react-testing-library';
import Counter from './Counter';
test('<Counter />', () => {
const { debug, getByTestId } = render(<Counter />);
const counterButton = getByTestId('counter-button')
debug();
expect(counterButton.tagName).toBe('BUTTON');
expect(counterButton.textContent).toBe('0');
});
//JS
import React, { Component } from 'react'
export default class Counter extends Component {
state = {
count: 0
}
render() {
const {count } = this.state
return (
<div>
<button type="button" data-testid='counter-button'>
{count}
</button>
</div>
)
}
}
Ok, so we need a test to define what happens when there is an event on that button. So first we need a way of watching events that are fired...
//Test File
import React from 'react'
import { render, fireEvent} from 'react-testing-library'; //Added FireEvent from React Testing Library
import Counter from './Counter';
test('<Counter />', () => {
const { debug, getByTestId } = render(<Counter />);
const counterButton = getByTestId('counter-button')
debug();
expect(counterButton.tagName).toBe('BUTTON');
expect(counterButton.textContent).toBe('0');
fireEvent.click(counterButton) //sends a click to the counter button
expect(counterButton.textContent).toBe('1'); //expect it to be one after the first click.
fireEvent.click(counterButton) //sends another click to the counter button
expect(counterButton.textContent).toBe('2'); //expect it to be two after the second click
debug() //This will output the DOM in the terminal after the additional clicks so its a good place to check whats happening.
});
At this point, our test suite should be telling us we are failing the test. Well, that's what happens if you have a button that does nothing so let's fix that...
import React, { Component } from 'react'
export default class Counter extends Component {
state = {
count: 0
}
count = () => {
this.setState( (prevState) => ({
count: prevState.count +1
}))
}
render() {
const {count } = this.state
return (
<div>
<button type="button"
onClick={this.count}
data-testid='counter-button'>
{count}
</button>
</div>
)
}
}
Cleanup, because testing isn't just always fun.
One little housekeeping touch. We want to ensure that after each test we clean things back up so it's all fresh for the next step. Handily React Testing Library gives us a cleanup method just for that purpose if we add that, that will make sure each test has a clean slate.
import { render, fireEvent, cleanup} from 'react-testing-library'; //Added from React Testing Library
afterEach(cleanup)
test('<Counter />', () => { //etc
Without that, you will get duplicate values in the DOM which is not ideal. It's easy to forget about but please don't!
Integration Testing with Forms
Ok so we have the basics down let's try and apply what we have learnt to a slightly more challenging but realistic example (but not that realistic, as you'll see)
Let's imagine we have a React app that is all about books and one of the features we want is the ability to add a new book. For that we might want a component for a new book with a book form component that is used inside :
- NewBook
- BookForm
I like to scaffold empty components before we get into the tests, but of course, that's up to you.
So I would like the NewBook component to:
- Show a heading that says "Enter a New Book"
- Show the Book Form
If we hold onto our test-id pattern from before it will be straightforward right? Here is our test...
import React from 'react'
import { render, cleanup } from 'react-testing-library';
import NewBook from './NewBook';
afterEach(cleanup)
test('<NewBook>', () => {
const {debug, getByTestId} = render(<NewBook/>) //Grab the tools we need for this next.
//Check Page Title is present and correct
const heading = getByTestId('page-title') //This id might be a good pattern between multiple components
expert(heading.tagName).toBe("H1") //Note the caps in 'h1'
expert(heading.textContent).toBe("Enter a New Book")
//Check Book Form is present
expert(queryByTestId('book-form')).toBeTruthy(); //Lets talk about this line.
debug()
});
We use queryByTestID
where we are a bit less sure about if it exists or not.
And... after checking that the test fails correctly, let's look at a first attempt New Book component:
import React, { Component } from 'react'
import BookForm from './BookForm'
export default class NewBook extends Component {
render() {
return (
<div>
<h1 data-testid='page-title'>Enter a New Book</h1>
<BookForm data-testid='book-form'/>
</div>
)
}
}
And we get a Failure message like this:
expect(received).toBeTruthy() Expected value to be truthy, instead received null
What gives?!
Remember at the start of the post, I said now React Testing Library looks at the resultant DOM whereas Enzyme looks at the Component. This is what makes it different.
In this case, the Component BookForm doesn't exist in the DOM, just its contents. So we need the data-testid to be on the form within the BookForm component. It is possible to mock the BookForm component (that's for another post) so that it can be picked up in the test, but the default 'thinking' of React Testing Library wants us to consider the result in the DOM. In other forms, it is integrated with the Book Form component.
Soon as we create the BookForm component with something that has the testId we can pass the test (though maybe not very robustly):
import React, { Component } from 'react'
export default class BookForm extends Component {
render() {
return (
<div>
<form data-testid='book-form'></form>
</div>
)
}
}
The resultant HTML from the debug output might help show what is going on if you are a bit lost:
<body>
<div>
<div>
<h1
data-testid="page-title"
>
Enter a New Book
</h1>
<div>
<form
data-testid="book-form"
/>
</div>
</div>
</div>
</body>
Phew, let's wrap this up
We covered the basics of React Testing using React Testing Library. In order to do this, we are going lightly over a few concepts and breezing over the quality of the tests. Hopefully, that is something I'll find time to do a deeper dive of later, my main goal is to get people up and running with the infrastructure of React testing.
However next time I think I will talk about the cool kid of Testing, Snapshot testing as that is cool... in the world of testing anyhow.
Top comments (0)