What are the benefits of automated testing on a project/application
What is the difference between a Unit, Integration, and End-to-End (E2E) test
Examples of how you should be testing your React applications
Advice for brainstorming the critical pieces of your app where automated testing would be useful
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
This guide will be as succinct and straight to the point as possible. In the future, we would like to provide more learning material, such as:
specific patterns of writing tests with open-source libraries (like RTL) and what functions provided by these libraries enable meaningful tests
implementing an advanced/complex level of automated testing at very large applications/organizations at scale
workshop material you can present to your developer team to make the automated testing efforts on your team more meaningful and valuable
more detailed advice on using testing to build a better product
If you think you could help out with the above and are interested in working in open-source/public, you can get involved with us on GitHub.
Unit testing - testing that our applications' smallest units (components) work as expected.
Integration testing - testing interactions between multiple units to see if they work as expected.
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.
Test - a piece of code written to assert if another piece of code is behaving as expected
Test Case - an individual point of functionality or "state" to test in the code that's being tested.
Test Runner - a tool that discovers your test files and interprets how to execute them
Test Suite - the collection of test cases for your application
Assertion - A comparison or check to verify some type of condition (for example, asserting if two string variables are equal or not)
Mocking / Mock - Overwriting/simulating external dependencies of your application for the sake of testing
Code Coverage - how much of the application code (0% - 100%) runs after the testing suite is executed. All things equal - 100% code coverage is the best end of the spectrum - but never an absolute requirement.
Regression Testing / Visual Regression - using screenshots of your application and comparing it to previous versions to alert you of visual changes. Heavy focus on the visual output of your application code, and not necessarily the behavioral aspect of it.
Below, you'll see the minimum steps required to start testing in your application.
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.
Dive in!
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.
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).
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)
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.
Snapshot testing - not terribly useful, alerts you to changes in the DOM rendered by component
Visual Regression - pixel-perfect screenshot comparison to know all changes, both big and small
Styles - Testing styles isn't the best use of time, but sometimes, it might make sense if the styles are a result of an interaction/event and important to the function of the app
UI Components (Unit Tests)
Custom Hooks & Context Providers (Unit Tests)
Global Stores (Unit Tests)
Custom Utils / App Functions (Unit Tests)
Page / Feature Components (Integration Tests)
Entire user flows / paths (E2E Tests)
Deep Dive - Behavioral Testing
UI Components (Unit Tests)
The smaller/basic components like Buttons, Cards, Toggles, etc., should mostly rely on props for their internal logic (they're more "pure") and, therefore should be easy to test.
Test output - Test that passing different props combinations results in the expected render output. What are this component's different render "states" or "variants" of output? Are there loading states or "empty" states if the data passed is null/empty?
Test interactivity - Does this component contain interactions that change what is rendered? Observe the interactivity and write that into a test
Controlled component interactivity - When components become "controlled" - they relinquish some of their internal state to the callee/parent. Typically, this is through the use of props and they would be named something like onClick, onToggle, onChange, etc. You should be testing that these props properly work and are called when expected and with the expected arguments if any are included.
Custom Hooks & Context Providers (Unit Tests)
As for testing a custom hook / context provider - it is recommended not to try to test the hook/context itself and instead consume the hook/context in a component for the sake of testing. The component can be one that's actually used in the application or is just a dummy component (that never gets loaded in the production bundle) for the sake of testing. Either way, writing a component to consume the underlying hook/context will be easier to test than trying to test just a hook/context in isolation.
The point we want to drive home for you is that for your test of a hook/context - try to do it all in one component (dummy or real, it doesn't matter).
On the idea of mocking contexts: is probably un-necessary for the most part. It can be useful sometimes to mock your context providers to return specific values if you need to test unit/integration behavior.
If you are insistent on testing custom hooks in isolation you can try this library: React Hooks Testing Library
Global Stores (Unit Tests)
Unlike the above, if you're using a library like Redux or Jotai, you CAN write some unit tests for your state logic. Any library worth using should probably have a page on how to write tests in said library, for example:
Most of the time, these are pure javascript functions kept somewhere for repeated use throughout the project. These should be simple to test because you don't have to worry about rendering anything. You can test the inputs and outputs of a simple function by passing different arguments as each function parameter and checking the return value.
Page / Feature Components (Integration Tests)
Test your components working together with other logic in your code like context providers, global stores, browser APIs, and third-party servers/APIs, as this is a more realistic simulation of your app being used by an actual user.
Test output - can you see that the child components are rendered correctly? Are there other visual queues that get rendered you should test for?
Test interactivity - typically in feature-level or page-level components, there are additional things you can test, some examples might be:
Form submission handlers, form validation, interactions that make third-party API calls, interactions that update stores/contexts, interactions that should cause a navigation change, interactions that trigger other expensive work (export to CSV, drawing on a canvas, etc.) that you could arguably mock
Test errors - since features/pages are where API interactions happen, its also typically where users can get errors / error-states in the app. Mock APIs to return errors and test that your frontend responds appropriately.
Entire user flows / paths (E2E Tests)
Tests that confirm an entire path a user might take is working as expected.
Examples: multi-step forms, multi-step authentication, multi-step config, etc.
Think of both the happy paths AND the unhappy paths, like when errors occur
Identifying what to test:
Which parts of your codebase breaking would be *really bad if they broke? This is where you start writing integration & E2E tests.
E-commerce: add to cart + checkout is the critical flow
Social networks: Authentication and main feed are the critical flows
Airbnb: Property search + booking
Is there an A/C (Acceptance Criteria) for a task, ticket, feature, or something that outlines the functionality? These are great starting points for test ideas in a feature/page-level integration test.
How are you testing your work locally? Every developer knows the game of ping-ponging between code editor and browser, checking every little behavior along the way. What are the things you are checking while building this feature locally? Use that to think about writing your test.
What are the paths or use cases a user of the app might take? Think of both happy paths AND sad paths. Test that both at least show the user the expected UX.
When in doubt, ask yourself, "Is this something the user would notice?"
What to avoid when writing tests
Testing that components are called with certain props
Testing render output is simpler and what the user would experience (users don't care about internal details of your app like props, so neither should your tests)
Tests that require or assume certain elements will appear in a certain order or hierarchy in the DOM
Sometimes, you'll make too many assumptions in your tests that generate false failures or break too easily. A trivial example of this would be querying all buttons in the DOM and then assuming the nth is the one we need (for example, something like querySelectorAll('.button')[1]). This can especially make an integration test of many interacting components very brittle and give false failures. Instead, query the button by its text, a data-id attribute, or something that wouldn't make this test so easy to break when code around it changes.
Something you can use in the browser to make queries for DOM nodes easier and more reliable is to open the developer tools, highlight/inspect the HTML being rendered by the component, and go to Elements → Inspect → Accessibility → Computed Properties. This will give you ways of querying the highlighted element via the accessibility computed properties.
Trying to test the internal state logic of components. Let's take a carousel, for example.
The internal logic of a carousel would have an index to keep track of the "current" item being shown. But that index could start at 0, or 1, or 500. Trying to test what index the carousel is keeping track of (too much of an implementation detail) - try to test what you expect the output to be of the carousel.
Again: "Would the user notice this?"
A user won't notice what index your carousel component starts at. But they will notice what the carousel is rendering, so test that!
Testing that components render certain child components with certain props (ensuring the data is passed through correctly). This is too much detail. Find ways to test the render output to assess whether the component behaves correctly.
Previous popular libraries like enzyme provided functions like .find() to locate child components rendered in a parent, and .props() to let us peek at the props supplied to a component. But again, these are all implementation details. Testing the API of the component is not the same as testing the behavior of the component. Try to test the behavior more than the API.
Besides, it's better to learn about trivial bugs like breaking prop name changes via TypeScript (during development)
All of these are just various ways of saying don't test too much implementation detail. Test what the user would notice.
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
// 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 placebeforeAll(() => {window.functionToMock =jest.fn().mockImplementation(() => {})})// B) You can mock the function to something different for each testbeforeEach(() => {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 filejest.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();})
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)