HomeAbout Me

React Testing: Avoid implementation details

By Daniel Nguyen
Published in React JS
July 23, 2025
2 min read
React Testing: Avoid implementation details

🧪 Avoiding Implementation Details in Your React Tests

When writing tests for your React components, there’s one golden rule that can make your code more robust, flexible, and maintainable:

āœ… Don’t test implementation details. āŒ Test what the user sees and does.

Let’s break down what that means, why it matters, and how to fix tests that break unnecessarily when you refactor your components.


šŸ’­ What Are Implementation Details?

Implementation details are the internal workings of your code — how an outcome is achieved. In contrast, public behavior is what the end user sees or interacts with.

Here’s an example with a simple utility function:

multiply(4, 5) // 20

The implementation of multiply() could be anything:

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

Or even:

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

The point? The user doesn’t care how you got the result — just that it’s correct.

Now let’s apply this principle to React component testing.


🚨 A Real Testing Problem

Here’s a simple Counter component:

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

In our test, we might do this:

const { container } = render(<Counter />)
fireEvent.click(container.firstChild)
expect(container.firstChild.textContent).toBe('1')

But what if we refactor?

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

šŸ’„ Boom. The test fails — even though the component works just fine for the user. Why? Because we wrote the test based on the component’s structure — an implementation detail.


āœ… How to Test the Right Way

Let’s fix that using React Testing Library’s user-centric queries.

import { render, screen, fireEvent } from '@testing-library/react'
import Counter from '../counter'
test('increments the counter', () => {
render(<Counter />)
const button = screen.getByRole('button', { name: '0' })
fireEvent.click(button)
expect(button).toHaveTextContent('1')
})

Why This Is Better:

  • āœ… screen.getByRole('button', { name: '0' }) matches how users experience the UI
  • āœ… Doesn’t rely on specific tag structure or DOM tree
  • āœ… Won’t break if we wrap elements or change HTML tags
  • āœ… Easy to refactor your component without rewriting tests

šŸ“š Learn more about screen: https://testing-library.com/docs/dom-testing-library/api-queries#screen


šŸ’Æ Bonus: Simulate Events Like a Real User

Even calling fireEvent.click(button) isn’t quite how users interact. When a user clicks a button, they:

  • Hover over it
  • Focus it
  • Mouse down
  • Mouse up
  • Then click!

That’s a lot of interaction, and fireEvent.click() only simulates one event.

So let’s go a step further.

Enter @testing-library/user-event

This library simulates real user behavior more accurately.

import userEvent from '@testing-library/user-event'
test('increments the counter', async () => {
render(<Counter />)
const button = screen.getByRole('button', { name: '0' })
await userEvent.click(button)
expect(button).toHaveTextContent('1')
})

Benefits of userEvent

  • šŸŽÆ Simulates real interaction flow
  • šŸŽ® Fires all relevant events in the right order
  • šŸ¤ Encourages user-first testing patterns

🧪 Explore how it works behind the scenes: click() source code


🧼 Recap: Avoid Implementation Details

āŒ Bad Practiceāœ… Good Practice
Query by container.firstChildUse screen.getByRole or screen.getByText
Assert with textContent === ...Use toHaveTextContent()
Use fireEvent.click()Use await userEvent.click()
Break tests when HTML changesBuild refactor-friendly, stable tests

🧠 Final Thoughts

Tests are only valuable if they continue to give you confidence after a refactor.

That means writing tests that focus on what the user sees and does, not how your component is built.

So next time you write a test, ask yourself:

ā€œWould this still pass if I changed the HTML structure but kept the same behavior?ā€

If not, it’s time to ditch the implementation details.


šŸ“‹ Practice + Reflect

Ready to practice this skill and retain what you learned? Fill out the elaboration and feedback form:

šŸ‘‰ Avoid Implementation Details – Feedback Form


Tags

#ReactTesting

Share

Previous Article
React Testing: Simple test with React Testing Library

Table Of Contents

1
šŸ’­ What Are Implementation Details?
2
🚨 A Real Testing Problem
3
āœ… How to Test the Right Way
4
šŸ’Æ Bonus: Simulate Events Like a Real User
5
🧼 Recap: Avoid Implementation Details
6
🧠 Final Thoughts
7
šŸ“‹ Practice + Reflect

Related Posts

React Testing: Testing custom hook
July 28, 2025
2 min
Ā© 2025, All Rights Reserved.
Powered By

Quick Links

About Me

Legal Stuff

Social Media