HomeAbout Me

React Testing 3: Avoid implementation details

By Daniel Nguyen
Published in React JS
September 04, 2025
2 min read
React Testing 3: Avoid implementation details

Avoid implementation details

Background

One of the most important things to remember about testing our software the way it is used is to avoid testing implementation details. “Implementation details” is a term referring to how an abstraction accomplishes a certain outcome. Thanks to the expressiveness of code, you can typically accomplish the same outcome using completely different implementation details. For example:

multiply(4, 5) // 20

The multiply function can be implemented in basically infinite ways. Here are two examples:

const multiply = (a, b) => a * b

vs

function multiply(a, b) {
let total = 0
for (let i = 0; i < b; i++) {
total += a
}
return total
}

One of those is more clear than the other, but that’s irrelevant to the point: The implementation of your abstractions does not matter to the users of your abstraction and if you want to have confidence that it continues to work through refactors then neither should your tests.

Here’s a React example of this:

function Counter() {
const [count, setCount] = React.useState(0)
const increment = () => setCount(c => c + 1)
return <button onClick={increment}>{count}</button>
}

Here’s one way you might access that button to click and assert on it:

const {container} = render(<Counter />)
container.firstChild // <-- that's the button

However, what if we changed it a bit:

function Counter() {
const [count, setCount] = React.useState(0)
const increment = () => setCount(c => c + 1)
return (
<span>
<button onClick={increment}>{count}</button>
</span>
)
}

Our tests would break!

The only difference between these implementations is one wraps the button in a span and the other does not. The user does not observe or care about this difference, so we should write our tests in a way that passes in either case.

So here’s a better way to search for that button in our test that’s implementation detail free and refactor friendly:

render(<Counter />)
screen.getByText('0') // <-- that's the button
// or (even better) you can do this:
screen.getByRole('button', {name: '0'}) // <-- that's the button

📜 Read up on screen here: https://testing-library.com/docs/dom-testing-library/api-queries#screen

Both of those resembles the way the user will search for our increment button.

📜 Read more about Testing Implementation Details and how to Avoid the Test User

📜 Learn more about the queries built-into React Testing Library from the query docs.

Exercise

Our current tests rely on implementation details. You can tell whether tests rely on implementation details if they’re written in a way that would fail if the implementation changes. For example, what if we wrapped our counter component in another div or swapped our message from a div to a span or p? Or what if we added another button for reset? Or what if instead of a button we switched to a clickable (and accessible) div? (That’s not an easy thing to do, so I recommend just using a button, but the point is hopefully clear).

Each of these things are implementation details that none of our users should know or care about, so this exercise is intended to help you learn to avoid implementation details by querying for and interacting with the elements in a way that is implementation detail free and refactor friendly.

Extra Credit

1. 💯 use userEvent

As it turns out, clicking these buttons is also a bit of an implementation detail. We’re firing a single event, when we actually should be firing several other events like the user does. When a user clicks a button, they first have to move their mouse over the button which will fire some mouse events. They’ll also mouse down and mouse up on the input and focus it! Lots of events!

If we want to be truly implementation detail free, then we should probably fire all those same events too. Luckily for us, Testing Library has us covered with @testing-library/user-event. This may one-day be baked directly into @testing-library/dom, but for now it’s in a separate package.

For this extra credit, swap out fireEvent for userEvent which you can get like so:

import userEvent from '@testing-library/user-event'

Once you’re done, look around in the code of @testing-library/user-event’s click method. It’s pretty interesting!

NOTE: In the latest version of @testing-library/user-event, all methods return a promise, so make sure you await the result of userEvent.click!

🦉 Elaboration and Feedback

After the instruction, if you want to remember what you’ve just learned, then fill out the elaboration and feedback form:

https://ws.kcd.im/?ws=Testing%20React%20Applications%20%F0%9F%A7%90&e=03%3A%20Avoid%20implementation%20details&em=

Solution

Full example

import * as React from 'react'
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Counter from '../../components/counter'
test('counter increments and decrements when the buttons are clicked', async () => {
render(<Counter />)
const increment = screen.getByRole('button', {name: /increment/i})
const decrement = screen.getByRole('button', {name: /decrement/i})
const message = screen.getByText(/current count/i)
expect(message).toHaveTextContent('Current count: 0')
await userEvent.click(increment)
expect(message).toHaveTextContent('Current count: 1')
await userEvent.click(decrement)
expect(message).toHaveTextContent('Current count: 0')
})

Tags

#React

Share

Previous Article
React Testing 2: simple test with React Testing Library

Table Of Contents

1
Avoid implementation details
2
Solution

Related Posts

React Testing 8: Testing custom hook
September 09, 2025
1 min
© 2025, All Rights Reserved.
Powered By

Quick Links

About Me

Legal Stuff

Social Media