What it means to not test implementation details in React

Are you unsure of what exactly you're trying to achieve when writing tests? Do you write tests based on other existing tests in your codebase? Perhaps you've quickly glanced at the React Testing Library documentation to try figure out "the right way" of testing.

At some point, you need to step back and think:

What does 'the right way' even mean when testing React?

What are we trying to achieve here?

If you've heard of React Testing Library, chances are you've heard the almost-slogan "test functionality, not implementation details".

Not testing implementation details requires a pretty large mindset shift if you're used to testing with Enzyme, as it provides several utilities explicitly for that purpose, such as find(), instance() and state().

For me, it helps to think from the perspective of a malicious user. Think about a sign-up form. I might try submit the form with no data to see what happens, maybe try submit the form without a password, or supply an invalid email address.

Just like that, you've got tests to write with no knowledge of implementation details:

Test 1 - no data

  1. Render the form
  2. Find the submit button, and click it
  3. Assert that the error message appears

Test 2 - no password

  1. Render the form
  2. Find the email field, and enter a random valid email address
  3. Find the submit button, and click it
  4. Assert that the error message appears

Test 3 - invalid email address

  1. Render the form
  2. Find the username field, and enter an invalid email address
  3. Find the submit button, and click it
  4. Assert that the error message appears

Side note: Once you've got a few tests like this, you can look at refactoring them to avoid duplicating code in your tests.

React Testing Library helps quite a bit with utility functions that let you easily write tests like this, but Enzyme will let you do the same thing too. The difference is, Enzyme won't stop you from going further, and writing tests against your props and state too, while React Testing Library goes out of its way to stop you from doing this.

The important thing is to test functionality, because it'll let you avoid writing brittle tests that rely too much on knowing which functions to call, and hopefully means you'll be rewriting your tests less often as your code gets refactored.

Conclusion

I find the easiest way to avoid testing implementation details is to pretend I'm not a developer, and try to perform a specific task in my application just like a user would.

For example, typing into a sign-up form, and trying to submit the form.

The tests I write would then seek to automate the clicking and typing the user would do (via React Testing Library's getByRole queries), rather than calling find(MyComponent) and calling functions directly.

(Shameless plug for the useEffect book I wrote below)

Tired of infinite re-renders when using useEffect?

A few years ago when I worked at Atlassian, a useEffect bug I wrote took down part of Jira for roughly one hour.

Knowing thousands of customers can't work because of a bug you wrote is a terrible feeling. To save others from making the same mistakes, I wrote a single resource that answers all of your questions about useEffect, after teaching it here on my blog for the last couple of years. It's packed with examples to get you confident writing and refactoring your useEffect code.

In a single afternoon, you'll learn how to fetch data with useEffect, how to use the dependency array, even how to prevent infinite re-renders with useCallback.

Master useEffect, in a single afternoon.

useEffect By Example's book cover