\ In this post, I’ll walk through my thought process for testing React components that rely on context, using Testing Library. My aim is to explore a different approach to testing these components, examining the pros and cons of using mocks versus testing without mocking the context. We’ll look at how each approach impacts the tests’ reliability, and I’ll share insights on when and why one method may be more beneficial than the other in real-world applications.
What you should knowThe ReactJS context emerged as a solution to a common problem in the structure of ReactJS components: prop drilling. Prop drilling occurs when we have a chain of components that need to access the same set of data. The context mechanism allows components to share the same set of data as long as the context itself is the first descendant.
\ In the reactjs documentation, the context for holding the theme is used, as other component might need this information the docs use the context to handle that instead of passing the value via props. Another example is the usage of context to hold the layout of the application, in the json-tool example the App.tsx wraps the application with a DefaultLayout context that is available for all the application.
The app for this exampleFor the example that follows the theme app will be used. It is an application that allows users to switch between light/dark themes. The app is also used in the reactjs official documentation. This application consist of a simple toggle that switches between light theme mode and dark theme mode. The application is as simple as it gets and we can plot everything in a single file:
\
import { createContext, useContext, useState } from 'react' const ThemeContext = createContext('light') function Page() { const theme = useContext(ThemeContext) return (current theme: {theme}
\ In this application, we have two main components: App and Page. The App component serves as the main component and contains the state for the current theme, which can be either “light” or “dark”. It also includes a button that toggles the theme between light and dark modes. The Page component is a child of App and consumes the theme context to display the current theme. The button in the App component is a simple toggle button that, when clicked, switches the theme and updates the context value accordingly.
\
\
The ignite for testingUsually in any application we would have to focus on what kind of test we want to do, and which slice we want to tackle. For example, we could target a single component, instead of the entire application. In our example, we will start with the Page component. Which will require us to use test-doubles to test it.
\
\ The test-double comes from the app structure itself, as it depends on the context, to change it, the value in the context needs to change as well.
Test-doublesTo get started with our testing approach with context in reactjs we will start writing the first test:
\
import { render, screen } from '@testing-library/react' import { Page } from './Page' describe('\ This test will pass as expected, given that the light theme is set to be the default one in the ThemeContext. We could even test drive this first example as well, however, the things get interesting in the second test, when we are interested in the dark theme. To get in to the dark theme, we need to start using test-doubles, given that we depend on the reactjs context to do that. The second test brings the vi.mock to the mix as well as the vi.mocked. Note that the second test to be written also required the first one to be changed.
\
import { render, screen } from '@testing-library/react' import { Page } from './Page' import { useContext } from 'react' vi.mock('react', () => { return { ...vi.importActual('react'), useContext: vi.fn(), createContext: vi.fn() } }) describe('\ Both test cases now are using a fake to test drive the application. If we change the return data from the context, the test will also change. The points of attention here are:
\
\
Without test-doublesThe next approach is to use the context embedded into our application, without isolating it or using any test-double. If we take this approach with TDD, we can start with a very simple test that simulates how the user will behave:
\
import { render, screen } from '@testing-library/react' import App from './App' import userEvent from '@testing-library/user-event' describe('\ Then following to the second test, that we would like to set the light theme by default:
\
import { render, screen } from '@testing-library/react' import App from './App' import userEvent from '@testing-library/user-event' describe('\ and last but not least the theme switching:
\
import { render, screen } from '@testing-library/react' import App from './App' import userEvent from '@testing-library/user-event' describe('\ Points of attention to this strategy:
\
\
Pros and cons of each approachIn this sections we will go over the pros and cons of each approach in regards to different properties.
Refactoring to propsUsing a test-double for the context makes the test fragile for this kind of change. Refactoring the usage of useContext with props automatically makes the test to fail even when the behaviour doesn’t. Using the option that doesn’t use test-doubles supports refactoring in that sense. In the book “Refactoring in Large Software Projects” the authors Stefan Roock and Martin Lippert depicted a scenario that is similar to what I am stating in here. One of the problems that they discussed is relatated with refactoring and changing class structures, they quote the C3 project:
\
About every 3 or 4 iterations we do a refactoring that causes us to toss or otherwise radically modify a group of classes. The tests will either go away or be changed to reflect the classes’ new behavior. We are constantly splitting classes up and moving behavior around. This may or may not affect the UnitTests\ Ideally to overcome this situation the unit tests would be written based on a “stable api” and the target system under test would not be touched, however, sometimes changing the api is also needed. For that, the solution proposed by the authors is to imply automated acceptance tests.
Creating a custom contextThe same happens to using a custom context instead of relying on the context provider from reactjs directly. Using the option without test-doubles enables refactoring.
ConclusionIn this guide, we explored how to test components that rely on context, without the need for test-doubles, making the tests more straightforward, closer to real user interactions and contrasting pros and cons of each approach. Whenever possible, using the simples approach that reflects the user interaction should be followed. However, when test-doubles are required, they should be used targeting the maintainability of the test code. Having a simple test enables refactoring in the production code with confidence.
Resources\
All Rights Reserved. Copyright 2025, Central Coast Communications, Inc.