search
HomeWeb Front-endJS TutorialTesting ReactJS Context - A Guide with test-doubles

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 know

  • What reactjs is used for (probably you have written some apps already)
  • What is vitest

What is react context

The 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 example

For 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 (
    <div>
      <p>current theme: {theme}</p>
    </div>
  )
}

function App() {
  const [theme, setTheme] = useState('light')
  return (
    <themecontext.provider value="{theme}">
      <button classname="{theme}" onclick="{()"> setTheme(theme === 'light' ? 'dark' : 'light')}
      >
        Toggle
      </button>
      <page></page>
    </themecontext.provider>
  )
}

export default App

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.

Testing ReactJS Context - A Guide with test-doubles

In the next section we will talk about slicing the components for testing.

The ignite for testing

Usually 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.

Testing ReactJS Context - A Guide with test-doubles

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-doubles

To get started with our testing approach with context in reactjs we will start writing the first test:

import { createContext, useContext, useState } from 'react'
const ThemeContext = createContext('light')

function Page() {
  const theme = useContext(ThemeContext)
  return (
    <div>
      <p>current theme: {theme}</p>
    </div>
  )
}

function App() {
  const [theme, setTheme] = useState('light')
  return (
    <themecontext.provider value="{theme}">
      <button classname="{theme}" onclick="{()"> setTheme(theme === 'light' ? 'dark' : 'light')}
      >
        Toggle
      </button>
      <page></page>
    </themecontext.provider>
  )
}

export default App

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'

describe('<page></page>', () => {
  it('should render light as default theme', () => {
    render(<page></page>)
    expect(screen.getByText('current theme: light')).toBeInTheDocument()
  })
})

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:

  • We are mocking reactjs context which hurst the "don't mock what you don't own principle"
  • The test become more verbose, since we are required to use mocking to do that
  • The two tests we have written don't reflect the user interaction with the application. We know that the theme will change when the toggle button is hit.

The completed code used in this section is available on GitHub

Without test-doubles

The 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 { Page } from './Page'
import { useContext } from 'react'

vi.mock('react', () => {
  return {
    ...vi.importActual('react'),
    useContext: vi.fn(),
    createContext: vi.fn()
  }
})

describe('<page></page>', () => {
  it('should render light as default theme', () => {
    vi.mocked(useContext).mockReturnValue('light')
    render(<page></page>)
    expect(screen.getByText('current theme: light')).toBeInTheDocument()
  })

  it('should render dark theme', () => {
    vi.mocked(useContext).mockReturnValue('dark')
    render(<page></page>)
    expect(screen.getByText('current theme: dark')).toBeInTheDocument()
  })
})

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('<app></app>', () => {
  it('should render toggle button', () => {
    render(<app></app>)
    expect(screen.getByText('Toggle')).toBeInTheDocument()
  })
})

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('<app></app>', () => {
  it('should render toggle button', () => {
    render(<app></app>)
    expect(screen.getByText('Toggle')).toBeInTheDocument()
  })

  it('should render light as default theme', () => {
    render(<app></app>)
    expect(screen.getByText('current theme: light')).toBeInTheDocument()
  })
})

Points of attention to this strategy:

  • Test-doubles are not required, it makes the test with less code
  • The behaviour of the test matches what the user will do in the real application

The completed code used in this section is available on GitHub

Pros and cons of each approach

In this sections we will go over the pros and cons of each approach in regards to different properties.

Refactoring to props

Using 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.

Creating a custom context

The 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.

Conclusion

In 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

  • Creating a custom context
  • The refactoring catalog
  • used to find how to mock specific part of a module with vitest
  • used to find how to fix type issue
  • testing library userEvent

Next Steps

  • Try testing more complex scenarios involving multiple contexts or nested providers.
  • While we avoided mocks in this guide, there are cases where mocking is necessary. Explore advanced mocking techniques for those scenarios.

By following these steps, you can continue to improve your testing skills and ensure your React applications are open to refactoring.

The above is the detailed content of Testing ReactJS Context - A Guide with test-doubles. For more information, please follow other related articles on the PHP Chinese website!

Statement
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
JavaScript in Action: Real-World Examples and ProjectsJavaScript in Action: Real-World Examples and ProjectsApr 19, 2025 am 12:13 AM

JavaScript's application in the real world includes front-end and back-end development. 1) Display front-end applications by building a TODO list application, involving DOM operations and event processing. 2) Build RESTfulAPI through Node.js and Express to demonstrate back-end applications.

JavaScript and the Web: Core Functionality and Use CasesJavaScript and the Web: Core Functionality and Use CasesApr 18, 2025 am 12:19 AM

The main uses of JavaScript in web development include client interaction, form verification and asynchronous communication. 1) Dynamic content update and user interaction through DOM operations; 2) Client verification is carried out before the user submits data to improve the user experience; 3) Refreshless communication with the server is achieved through AJAX technology.

Understanding the JavaScript Engine: Implementation DetailsUnderstanding the JavaScript Engine: Implementation DetailsApr 17, 2025 am 12:05 AM

Understanding how JavaScript engine works internally is important to developers because it helps write more efficient code and understand performance bottlenecks and optimization strategies. 1) The engine's workflow includes three stages: parsing, compiling and execution; 2) During the execution process, the engine will perform dynamic optimization, such as inline cache and hidden classes; 3) Best practices include avoiding global variables, optimizing loops, using const and lets, and avoiding excessive use of closures.

Python vs. JavaScript: The Learning Curve and Ease of UsePython vs. JavaScript: The Learning Curve and Ease of UseApr 16, 2025 am 12:12 AM

Python is more suitable for beginners, with a smooth learning curve and concise syntax; JavaScript is suitable for front-end development, with a steep learning curve and flexible syntax. 1. Python syntax is intuitive and suitable for data science and back-end development. 2. JavaScript is flexible and widely used in front-end and server-side programming.

Python vs. JavaScript: Community, Libraries, and ResourcesPython vs. JavaScript: Community, Libraries, and ResourcesApr 15, 2025 am 12:16 AM

Python and JavaScript have their own advantages and disadvantages in terms of community, libraries and resources. 1) The Python community is friendly and suitable for beginners, but the front-end development resources are not as rich as JavaScript. 2) Python is powerful in data science and machine learning libraries, while JavaScript is better in front-end development libraries and frameworks. 3) Both have rich learning resources, but Python is suitable for starting with official documents, while JavaScript is better with MDNWebDocs. The choice should be based on project needs and personal interests.

From C/C   to JavaScript: How It All WorksFrom C/C to JavaScript: How It All WorksApr 14, 2025 am 12:05 AM

The shift from C/C to JavaScript requires adapting to dynamic typing, garbage collection and asynchronous programming. 1) C/C is a statically typed language that requires manual memory management, while JavaScript is dynamically typed and garbage collection is automatically processed. 2) C/C needs to be compiled into machine code, while JavaScript is an interpreted language. 3) JavaScript introduces concepts such as closures, prototype chains and Promise, which enhances flexibility and asynchronous programming capabilities.

JavaScript Engines: Comparing ImplementationsJavaScript Engines: Comparing ImplementationsApr 13, 2025 am 12:05 AM

Different JavaScript engines have different effects when parsing and executing JavaScript code, because the implementation principles and optimization strategies of each engine differ. 1. Lexical analysis: convert source code into lexical unit. 2. Grammar analysis: Generate an abstract syntax tree. 3. Optimization and compilation: Generate machine code through the JIT compiler. 4. Execute: Run the machine code. V8 engine optimizes through instant compilation and hidden class, SpiderMonkey uses a type inference system, resulting in different performance performance on the same code.

Beyond the Browser: JavaScript in the Real WorldBeyond the Browser: JavaScript in the Real WorldApr 12, 2025 am 12:06 AM

JavaScript's applications in the real world include server-side programming, mobile application development and Internet of Things control: 1. Server-side programming is realized through Node.js, suitable for high concurrent request processing. 2. Mobile application development is carried out through ReactNative and supports cross-platform deployment. 3. Used for IoT device control through Johnny-Five library, suitable for hardware interaction.

See all articles

Hot AI Tools

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Undress AI Tool

Undress AI Tool

Undress images for free

Clothoff.io

Clothoff.io

AI clothes remover

Video Face Swap

Video Face Swap

Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Tools

Atom editor mac version download

Atom editor mac version download

The most popular open source editor

SublimeText3 Linux new version

SublimeText3 Linux new version

SublimeText3 Linux latest version

SublimeText3 Mac version

SublimeText3 Mac version

God-level code editing software (SublimeText3)

SublimeText3 English version

SublimeText3 English version

Recommended: Win version, supports code prompts!

SAP NetWeaver Server Adapter for Eclipse

SAP NetWeaver Server Adapter for Eclipse

Integrate Eclipse with SAP NetWeaver application server.