This site runs best with JavaScript enabled.

Testing In React

Last Updated November 10, 2020


Testing is a crucial weapon of any developer's arsenal. Testing ensures your code actually does what you coded it to do. Therefore, understanding how to write good tests is vital.

Having said that, this is not an article on what testing is, nor is it intended to teach you how write tests. The intention of this is teach you how to write tests in React. Now, along with that, you may learn some things about what testing is or how to write tests in general. But, in the course of learning how to test in React you may end up learning some of the fundamentals, as well.

Why Write Tests?

We can all agree testing is important. After all, there are entire divisions of IT departments specifically devoted to testing software. Why, then, should we test? If there are people paid to do it, why should we? Well, first of all, programmatic testing is faster than manual testing. So, increased efficiency is one reason. Secondly, programmatic testing is more deterministic, less error prone. Which, in turn, means increased accuracy. A by-product of increased efficiency and accuracy is peace of mind. And that peace of mind extends from the hands developing the software all the way to end user using the software.

There's also the idea of professionalism at play here. As a software developer, you are a professional. Much like any other professional: doctors, lawyers, architects, scientists, we are the experts. That's worth repeating: When it comes to developing software, we are the experts. That said, there's a degree of responsibility that comes along with being an expert in some field. Writing good, working software is part of that responsibility. Since writing tests increases the likelihood of good, working software, we ought to write tests. Even further, one could say, with some valid degree of authority, that not writing tests is unprofessional. And, in certain fields, such as where good, working software could very well be the determining factor of someone's life, for example, medical software or transportation (plane, train, car, truck, spaceship) software, one could say not writing tests is dangerous and reckless.

For more insights into why developers should be writing tests, see Uncle Bob Martin's talk on Professionalism. It's well-worth the 45 minutes.

Not All Testing Is Created Equal

Programmatic testing falls under a few different categories. Those categories have different names depending who you ask. For our purposes we'll call them unit, functional, and integration tests. All three serve different purposes. Our focus will mainly be on unit and functional tests.

Tools

Just as this is not an article on what testing is or how to write tests, this is also not intended to teach you all about the specific libraries we'll be using. You'll learn stuff about them, sure. But, if you want to understand them any more than a working capacity then you'll need to seek that out yourself.

We'll be using Jest as our test runner. Under the hood it uses a tool called jsdom, which is basically a browser imitator built in Javascript, running on Node.js.

We'll also introduce React Testing Library as a way to help us write better tests without relying on implementation details. More on that later.

Example

Before we start writing code, test or not, we need to understand what we're going to implement. Here are the requirements:

In this app you are greeted when you enter. Now, new requirements have come in to say goodbye when you leave.
See the acceptance criteria below:
- Use the containment pattern to create a `PrettyBorder` component.
- Use the specialization pattern to create one, general, message component.
- That message component should be *composed* or *implemented* for each type of message - in our case, greeting and goodbye.
- The goodbye message should display when the Leave button is clicked.
- Only one message is displayed at a time.
- No message is displayed on startup (this makes it a little tricky)

Here's a sample of the functionality:

With these new requirements we can now write some tests. Let's take them line by line:

Use the containment pattern to create a PrettyBorder component.

This is an implementation detail. It has no bearing on the app's functionality. We do not need to test this.

Use the specialization pattern to create one, general, message component.

Again, this is an implmentation detail, no need to test it.

That message component should be composed or implemented for each type of message - in our case, greeting and goodbye.

This simply refers to the requirement from above. No test.

The goodbye message should display when the Leave button is clicked.

Finally, our first "action". This is something that is testable. Do you see the difference in this requirement over the others? It's a new expectation.

Expectations Over Implementations

You'll hear that a lot when dealing with frontend testing. Expectations Over Implementations. It's a phrase used to indicate we write tests according to expectations, not implementations.

What this means is I can rewrite my code as much as I want and as long as my expectations stay the same I never have to modify my tests. This is in stark contrast to how you may have written tests in the past. For example, in the .NET world you often write unit tests against specific methods. And there's nothing wrong with that. However, in the world of frontend development we have the priviledge of a virtual DOM. That means we have the ability to actually pretend to be the user programatically. That's an opportunity we cannot pass up. When it comes to testing, you want your tests as close to the user as possible. And let's face it, you can't get much closer than programatically clicking button and expecting behaviors can you?

That's why I choose React Testing Library to write tests. It was written with expectation testing in mind.

To give a real, concrete example of what I'm talking about let's look at the code for the component we'll be testing.

/////////////////////////////////
// Some code excluded for brevity. See bottom of the article for full code.
/////////////////////////////////
class Sample extends React.Component {
constructor(props) {
super(props)
this.state = { status: this.STATUS.neverEntered }
}
STATUS = { neverEntered: 'neverEntered', entered: 'entered', left: 'left' }
handleEnter = () => this.setState({ status: this.STATUS.entered })
handleLeave = () => this.setState({ status: this.STATUS.left })
handleReset = () => this.setState({ status: this.STATUS.neverEntered })
render() {
let message
let button = <Outside handleClick={this.handleEnter} />
const status = this.state.status
if (status === this.STATUS.entered) {
message = <GreetingMessage />
button = <Inside handleClick={this.handleLeave} />
} else if (status === this.STATUS.left) {
message = <GoodbyeMessage />
button = <Outside handleClick={this.handleEnter} />
}
return (
<div style={{padding: '20px', backgroundColor: 'lightgray', borderRadius: '20px', color: 'black'}}>
{button}{ ' ' }<Reset handleClick={handleReset} />
{message}
</div>
)
}
}

Now, let's say we want to refactor this to be a function component.

/////////////////////////////////
// Some code excluded for brevity. See bottom of the article for full code.
/////////////////////////////////
function Sample() {
const STATUS = { neverEntered: 'neverEntered', entered: 'entered', left: 'left' }
const [ status, setStatus ] = useState(STATUS.neverEntered)
const handleEnter = () => setStatus(STATUS.entered)
const handleLeave = () => setStatus(STATUS.left)
const handleReset = () => setStatus(STATUS.neverEntered)
let message
let button = <Outside handleClick={handleEnter} />
if (status === STATUS.entered) {
message = <GreetingMessage />
button = <Inside handleClick={handleLeave} />
} else if (status === STATUS.left) {
message = <GoodbyeMessage />
button = <Outside handleClick={handleEnter} />
}
return (
<div style={{padding: '20px', backgroundColor: 'lightgray', borderRadius: '20px', color: 'black'}}>
{button}{ ' ' }<Reset handleClick={handleReset} />
{message}
</div>
)
}

Even though those components have two completely different implementations their expectations are the same. Because of that the tests we are about to write will work seamlessly for either one.

The First Test

Let's look at the first test.

it('should show Goodbye message after clicking Leave', () => {
const { getByText, queryByText } = render(<Solution />) // this simply renders the components and pulls out some useful methods from the @testing-library/react library
const enterButton = getByText(/Enter/i) // first, we get the Enter button
enterButton.click() // then we click the Enter button
const leaveButton = getByText(/Leave/i) // once the Enter button is clicked, the Leave button should be available
leaveButton.click() // then we click it, hopefully displaying our new goodbye message
const goodbye = queryByText(/See you later!/i) // next, we'll get that goodbye message and verify it exists (the purpose of the test)
expect(goodbye).toBeInTheDocument() // all we want to do it make sure it exists on the page
})

The name of the test says it all. it('should show Goodbye message after clicking Leave'. Walking through the code (see the comments), you can see what we're doing is just programmatically doing what it would take to manually test this requirement.

If this hasn't been coded yet the test should fail.

By the way, you can run your tests with yarn test from your terminal in the root directory (the same directory that your package.json file exists).

I recommend writing your test first, before writing any code. Why? Because it allows you to "spec" out what you expect your component to do. This is called Test Driven Development, or TDD. It's a discipline more than anything, but if you subscribe to it, you'll find the upfront time cost ends up saving time in the near and distant future.

If you haven't yet, go ahead and write the code to attempt to make this test pass. We never even have to load the browser! - you should always take a look, though. It's possible to write the wrong test!

**I'll put the full working code example at the end of this article.

Only one message is displayed at a time.

Here we have another expectation. We need to make sure that for each button click, Enter or Leave, that the proper message is displayed.

it('should show Greeting message after clicking Enter', () => {
const { getByText } = render(<Start />)
const enterButton = getByText(/Enter/i)
enterButton.click()
const greeting = getByText(/Hello there!/i)
expect(greeting).toBeInTheDocument()
})
it('should hide Greeting message after clicking Leave', () => {
const { getByText, queryByText } = render(<Start />)
const enterButton = getByText(/Enter/i)
enterButton.click()
const leaveButton = getByText(/Leave/i)
leaveButton.click()
const greeting = queryByText(/Hello there!/i)
expect(greeting).toBeNull() // note we are making sure the greeting does NOT exist here
})

The two tests above ensure that when Enter is clicked the Greeting message is displayed and that when the Leave button is clicked the message goes away.

Let's look at how we would test that the Goodbye message goes away when we click Leave.

it('should hide Goodbye message after clicking Enter/Leave/Enter', () => {
const { getByText, queryByText } = render(<Solution />)
let enterButton = getByText(/Enter/i)
enterButton.click()
const leaveButton = getByText(/Leave/i)
leaveButton.click()
let goodbye = queryByText(/See you later!/i)
expect(goodbye).toBeInTheDocument()
enterButton = getByText(/Enter/i)
enterButton.click()
goodbye = queryByText(/See you later!/i)
expect(goodbye).toBeNull()
})

This test runs a little longer. You get an indication that it will take more steps performed to test this behavior just by the title it('should hide Goodbye message after clicking Enter/Leave/Enter'. The title clearly lays out we'll need three button clicks to achieve our desired state.

Notice the first expect(), expect(goodbye).toBeInTheDocument(). That's not really part of the test. However, I chose to put it there because it verifies an expectation that we need in order to get to the state we want to be in. Let's imagine I left that line out. Now, the test is that the Goodbye message does not exist when we're finished with our actions. If that first expect() did not exist and the test passed, then we never know if clicking Leave actually hid the message. The message could've never existed in the first place! Now, I understand this is actually tested in the prior requirement. So, having that line there isn't necessary, nor is it even considered a best practice, from what I know. It's simply something I do to give me more peace of mind when writing tests.

A potential gotcha

Here's something important to note, that might otherwise get overlooked. Notice that enterButton is declared with let. That's because we need to go get it again, it needs a second assignment. That's because that instance of the enterButton actually went away after it was clicked and the Leave button took its place. So, after clicking Leave with leaveButton.click() and before clicking Enter again with enterButton.click() we need to go get Enter button again. This is an important point to remember, as it could cause some headache trying to debug a busted test.

No message is displayed on startup (this makes it a little tricky)

Here we also have another behavior or expectation. However, it also existed in the Start component so we already have a test for it.

it('should not show Greeting message on render', () => {
const { queryByText } = render(<Start />)
const greeting = queryByText(/Hello there!/i)
expect(greeting).toBeNull()
})

The (this makes it a little tricky) note is really only for the development portion. However, having the test already there can give us confidence in what we're writing when implementing it.

That's it! We're done! I hope this example serves to help you get started in writing good frontend tests. If you have any questions or suggestions about the article or anything else, for that matter, let me know! I'm happy to help in any way I can!

Now, here's the full component code I promised.

function PrettyBorder(props) {
return (
<div style={{ border: '5px solid purple', margin: 20, padding: 20, fontSize: 16 }}>
{props.children}
</div>
)
}
const Message = (props) => <PrettyBorder>{props.content}</PrettyBorder>
const GreetingMessage = () => <Message content='Hello there!' />
const GoodbyeMessage = () => <Message content='See you later!' />
const Inside = (props) => <button onClick={props.handleClick}>Leave</button>
const Outside = (props) => <button onClick={props.handleClick}>Enter</button>
const Reset = (props) => <button onClick={props.handleClick}>Reset</button>
function Sample() {
const STATUS = { neverEntered: 'neverEntered', entered: 'entered', left: 'left' }
const [ status, setStatus ] = useState(STATUS.neverEntered)
const handleEnter = () => setStatus(STATUS.entered)
const handleLeave = () => setStatus(STATUS.left)
const handleReset = () => setStatus(STATUS.neverEntered)
let message
let button = <Outside handleClick={handleEnter} />
if (status === STATUS.entered) {
message = <GreetingMessage />
button = <Inside handleClick={handleLeave} />
} else if (status === STATUS.left) {
message = <GoodbyeMessage />
button = <Outside handleClick={handleEnter} />
}
return (
<div style={{padding: '20px', backgroundColor: 'lightgray', borderRadius: '20px', color: 'black'}}>
{button}{ ' ' }<Reset handleClick={handleReset} />
{message}
</div>
)
}
Share article

Join the Newsletter

This just means you'll receive each weekly blog post as an email from me and maybe an occasional announcement 😀


Disclaimer: Views and opinions expressed on this blog are my own and are in no way representative of my employer.