Testing Context-Wrapped React Components
In our frontend application, we make use of components that subscribe to context providers, and those providers often contain state-updating logic. Test setup for context-wrapped components is often confusing, and quite frankly, a pain.
There are two main options for testing these context-wrapped components:
- Use a static context provider with hard-coded data.
- Use your custom context provider, allowing the component under test to trigger any logic contained in the provider.
In this post, I’ll discuss the two approaches and why you might use one over the other.
Basics of React Context
- Context allows you to share data that is global to a component tree without having to pass props between every parent and child
- You can create a context with
- a child component wrapped in a Provider and then a Consumer is subscribed to data changes from the Provider
- For functional components, you can use the
useContexthook to subscribe to data changes from the Provider without explicitly wrapping your child component in a Consumer
Below is an example of a pattern we use often to create components that wrap the actual Provider component. In this example,
MyFormProviderimplements logic that controls the
value prop that is passed to React’s
Provider component on line 14.
As I said above, we have two options when we want to test a component that subscribes to
MyFormContext. For this discussion, what our component specifically does is less important, but for the sake of this example, let’s say it’s a component that takes line items from the context, and allows the user to bulk select them.
Approach 1: Use a static Provider, passing in hard-coded data to
- requires data setup for
valueso that you can get your DOM into the expected state
- you won’t have to worry about mocking out any other logic that would be called in a custom provider, like an API call
- often can be tested synchronously, since the component renders in the state you expect
- sometimes doesn’t allow you to test the user interaction, which may make the test more succinct but obscure how the DOM can get into this state
Approach 2: Use your custom provider with all its logic
- requires only initial data setup, then relies on simulated interactions to get the DOM into the expected state
- more closely mirrors what the component would look like in production
- allows you to test the way a real user would interact with your component
- may require more test setup, if you need to mock out logic that is used in the customer provider, like API calls
- may need to be tested
asyncif user interactions trigger async state updates in the custom provider
When to Use Which Approach
Neither way is “right”, but my recommendation is to prefer testing with the custom context provider for these reasons:
- It better matches the way the component is rendered in production
- You have to do less thinking about setting up data in the right state. You just need to think about the initial inputs to the context, and then you can trust that any other values that it works with will be properly updated (assuming that context component itself has been unit tested.)
- It allows you to test the feature more as a user would, without manipulating the data to get the DOM into a certain state
However, here’s when I would recommend testing with the static provider:
- if you are testing lower-level children that render data from the provider but don’t update that data
- if the test setup required to render the custom provider is so onerous that it provides a lot of friction to writing the test. In this case, use the custom provider at the top level component for an integration-style test, and use the static provider for unit-testing the children.