A Wide View of Automated Testing in React Apps


Given the focus on React of this website - most of the testing concepts presented in this guide will be described in a way that its used with React. This might not be the best introduction to testing if you've never heard of it before.

Looking for Contributors

What is the point of automated testing?

Let's split this into 2 categories in case you have to make the argument for testing to your non-technical superiors.

The 3 Types of Tests

  1. Unit testing - testing that our applications' smallest units (components) work as expected.
  2. Integration testing - testing interactions between multiple units to see if they work as expected.
  3. End-to-end ("E2E", "Functional") testing - testing entire workflows/paths that users can take in your application.

Next, we've included a short glossary of frequently used words when talking about automated testing. These are simple definitions that might not cover every detail about each term, but should give you an idea of what they mean.

Below, you'll see the minimum steps required to start testing in your application.

How to Get Started & Write Your First Test

  1. Choose a test-runner
    1. Jest or Vitest for unit/integration tests
    2. Cypress or Selenium or Playwright for E2E tests
  2. Choose a testing library
    1. React Testing Library is the best modern choice. Enzyme is an alternative, but we wouldn't recommend it due to the patterns of tests the API encourages developers to write.
  3. Dive in!
    1. If you've never written or seen a unit test in a React app before, you could watch Jack Herrington go through a few examples in this YouTube video.
    2. Be aware of this configuration requirement if your application uses a React version ≥ 18 and you're not going to use React Testing Library (which handles it for you).
    3. Also be aware of new features added to React 18 that might break your test suite if the underlying tools haven't taken them into account (e.g. Concurrent mode, Suspense)

What to Test In A React App

In an ideal world, the answer to this question is everything you control. And 99% of the time that can be divided into two categories: Visual and Behavioral testing.

Everything that you test that isn't "Visual" is probably going to fall in the "Behavior" category. This is quite a large net - honestly, entire books could probably dedicated to writing effective automated tests of component behavior. Below are some examples of each.

Deep Dive - Behavioral Testing

FAQs

General Tips

  • Refactoring your code shouldn't break tests.
    • Since the traditional definition of a refactor would include NOT changing the functionality/output (and simply making the code more efficient/maintainable) - if a test breaks during a refactor session, then you had to have changed functionality, or the test was too brittle and assumed too much implementation detail. This is a "false failure", by the way.
  • Use custom rendering to make it easier to simulate your application in your test suite
  • Use Mock Service Workers (MSW) for mocking your API endpoints
  • Use faker for generating large amounts of fake datasets

Tips

Code Tips

// Mocking global APIs like window.scrollTo that you don't import from anywhere

// A) You can have the same mock for ALL of your tests in one place
beforeAll(() => {
  window.functionToMock = jest.fn().mockImplementation(() => {})
})

// B) You can mock the function to something different for each test
beforeEach(() => {
  window.functionToMock = jest.fn()
})

test('description', () => {
  window.functionToMock.mockImplementation(() => {}) // mock function A
})

test('description', () => {
  window.functionToMock.mockImplementation(() => {}) // mock function B
})

// How to mock functions you are importing from another file/source
// This is useful for mocking functions in your code that hit API endpoints (if you're not using the MSW pattern)
import { someFunction } from 'third-party-npm-dependency'

// tell jest we will mock it in this file
jest.mock('third-party-npm-dependency')

test('the first test', () => {
  someFunction.mockImplementation(() => {}) // pass a mock function here
})

test('another test where we want a different return value from someFunction', () => {
  someFunction.mockImplementation(() => {}) // you can change it in each test with mockImplementation();
})

More Tips for Using React Testing Library (RTL)

Watch Testing Library: everybody uses it, but nobody understands it by Matan Borenkraout.

  • RTL has a very rich API for querying/selecting DOM nodes rendered by your components - it's very useful for making tests less brittle and reliant on implementation details
    • You can find a similar tool in browsers by going to Elements → Inspect → Accessibility → Computed Properties
    • The Testing Playground can also be helpful in finding creative but robust ways of finding DOM nodes in your render output
  • When firing events in your tests, 99% of the time, you should prefer userEvent over fireEvent, because userEvent simulates an event closer to how a browser would (including propagation and other events that get fired like mouseUp / mouseDown)

Misc Reading