enzyme: useContext hook not working with shallow

The useContext hook does not work with shallow rendering and passing in context. Are there any workarounds for now?

Current behavior

The value returned by useContext in a function component appears to be empty when using shallow and passing in a context argument. (The same problem also occurs when wrapping the component in the appropriate context Provider directly and then calling .dive()).

Here is a minimal test case, also reproduced at https://codesandbox.io/s/nice-blackwell-yz7tn in the index.spec.js file.

import React, { useContext } from "react";
import PropTypes from "prop-types";

import Enzyme, { shallow } from "enzyme";
import Adapter from "enzyme-adapter-react-16";

Enzyme.configure({ adapter: new Adapter() });

const MyContext = React.createContext({});

export const MyComponent = () => {
  const { myVal } = useContext(MyContext);

  return <div data-test="my-component">{myVal}</div>;
};

it("renders the correct text", () => {
  MyComponent.contextTypes = {
    myVal: PropTypes.any
  };
  const wrapper = shallow(
    <MyComponent />,
    {context: {myVal: 'foobar'}}
  );
  expect(wrapper.text()).toEqual("foobar"); // expected "foobar" received ""
});

Expected behavior

The text in the example above is expected to be "foobar", but it’s actually "". In general, the value returned from useContext appears to be undefined.

Note that using mount instead of shallow causes the test to pass.

Also note: the codesandbox above has a second file (class.spec.js) in which a hack is employed that makes the test pass, which uses the legacy contextTypes. But this only appears to work with classes and this.context, not with useContext.

Your environment

enzyme 3.10.0 enzyme-adapter-react-16 1.14.0 react 16.8.6 react-dom 16.8.6

API

  • shallow
  • mount
  • render

Version

library version
enzyme
react
react-dom
react-test-renderer
adapter (below)

Adapter

  • enzyme-adapter-react-16
  • enzyme-adapter-react-16.3
  • enzyme-adapter-react-16.2
  • enzyme-adapter-react-16.1
  • enzyme-adapter-react-15
  • enzyme-adapter-react-15.4
  • enzyme-adapter-react-14
  • enzyme-adapter-react-13
  • enzyme-adapter-react-helper
  • others ( )

About this issue

  • Original URL
  • State: open
  • Created 5 years ago
  • Reactions: 46
  • Comments: 36 (10 by maintainers)

Commits related to this issue

Most upvoted comments

I found some workarounds for shallow rendering with React.useContext, Material UI’s makeStyles, and react-redux v7.x.x. See below for some high level notes and my codesandbox for examples.

Components that consume context using React.useContext()

  • use just.spyOn(React, 'useContext').mockImplementation((context) => 'context_value' ) to return a context value. (Thanks to @mstorus’s example above for the idea of mocking module implementations). @garyyeap, you can do this to mock useContext for code you don’t control, but that won’t help you with react-redux 7.1. See the Redux section below for the redux workaround.
  • Wrapping your component in <Context.Provider value={myContextValue}/> does not work.
  • Using the wrappingComponent option does not work.

Components that consume context using <Context.Consumer />

  • Wrapping your component in <Context.Provider value={myContextValue}/> works.
  • Using the wrappingComponent option works.

Material UI’s makeStyles

  • Uses the React.useContext() hook. You’ll have to mock return values for React.useContext() for both ThemeContext and StylesContext to get this to work. Yeah, this is pretty gross, but it’s all I got.

Redux - Redux has some fancy logic for using context. It looks like you can optionally pass the store or a custom context into a connected component.

  • Inject store into connected component. E.G. <MyConnectedComponent store={mockStore}/>
  • Inject custom context into connected component. E.G. <MyConnectedComponent context={MockReduxContext}/>

It’s pretty disappointing that shallow rendering’s been broken for so long and that our options are limited to switching to mount or finding brittle workarounds like these.

The workaround I’m using for now is the one described in this article; wrap useContext in a function and then use jest to mock the implementation.

https://codesandbox.io/s/crimson-mountain-vo7sk

MyContext.js

import React, { useContext } from "react";

const MyContext = React.createContext({});
export default MyContext;
export const useMyContext = () => useContext(MyContext);

index.spec.js

import React from "react";

import Enzyme, { shallow } from "enzyme";
import Adapter from "enzyme-adapter-react-16";

import MyContext, { useMyContext } from "./MyContext";
import * as MyContextModule from "./MyContext";

Enzyme.configure({ adapter: new Adapter() });

export const MyComponent = () => {
  const { myVal } = useMyContext(); // instead of useContext(MyContext)

  return <div data-test="my-component">{myVal}</div>;
};

it("renders the correct text", () => {
  jest.spyOn(MyContextModule, "useMyContext").mockImplementation(() => ({
    myVal: "foobar"
  }));

  const wrapper = shallow(
    <MyContext.Provider>
      <MyComponent />
    </MyContext.Provider>
  ).dive();
  expect(wrapper.text()).toEqual("foobar");
});

@twharmon, try this for named exports.

E.G.

import * as ReactAll from 'react';

// React is ReactAll.default
// useContext is ReactAll.useContext
jest.spyOn(ReactAll, 'useContext').mockImplementation(() => {...}

For me, just.spyOn(React, 'useContext').mockImplementation(() => {...} ) will work if my component has const ctx = React.useContext(MyContext) but not with const ctx = useContext(MyContext).

I also am having this problem, and would MEGA appreciate any workarounds or fixes that might come 😃

For me, just.spyOn(React, 'useContext').mockImplementation(() => {...} ) will work if my component has const ctx = React.useContext(MyContext) but not with const ctx = useContext(MyContext).

@twharmon I don’t have to do that if I do

jest.mock("react", () => ({
  ...jest.requireActual("react"),
  useContext: () => 'context_value'
}));

instead of

jest.spyOn(React, 'useContext').mockImplementation(() => 'context_value')

for the sake of explicitness, here’s an example of directly using a context provider and calling .dive(), which also doesn’t work:

https://codesandbox.io/s/elastic-zhukovsky-w5pjc index.spec.js

import React, { useContext } from "react";

import Enzyme, { shallow } from "enzyme";
import Adapter from "enzyme-adapter-react-16";

Enzyme.configure({ adapter: new Adapter() });

const MyContext = React.createContext({});

export const MyComponent = () => {
  const { myVal } = useContext(MyContext);

  return <div data-test="my-component">{myVal}</div>;
};

it("renders the correct text", () => {
  const wrapper = shallow(
    <MyContext.Provider value={{ myVal: "foobar" }}>
      <MyComponent />
    </MyContext.Provider>
  ).dive();
  expect(wrapper.text()).toEqual("foobar"); // expected "foobar" received ""
});

@ljharb is the reason why this variation also doesn’t work the same as you mentioned above?

I was able to workaround it the following way:

const MyProvider: React.FC<{
  children: React.ReactElement;
}> = ({ children }) => {
  const originalUseContext = React.useContext;
  const useContextSpy = jest.spyOn(React, 'useContext');
  useContextSpy.mockImplementation(contextType =>
    contextType === MyContext ? 'context_value' : originalUseContext(contextType)
  );

  React.useEffect(() => {
    return () => {
      useContextSpy.mockRestore();
    };
  }, [useContextSpy]);

  return children;
};

and its usage:

const wrapper = shallow(<MyComponent />, {
  wrappingComponent: MyProvider,
  wrappingComponentProps: {},
});

You can pass context value via wrappingComponentProps if you want

I’m having same issue with using react-redux 7.1, but I can’t change the way how react-redux use the useContext(). Is there any other workaround?

The workaround I’m using for now is the one described in this article; wrap useContext in a function and then use jest to mock the implementation.

https://codesandbox.io/s/crimson-mountain-vo7sk

MyContext.js

import React, { useContext } from "react";

const MyContext = React.createContext({});
export default MyContext;
export const useMyContext = () => useContext(MyContext);

index.spec.js

import React from "react";

import Enzyme, { shallow } from "enzyme";
import Adapter from "enzyme-adapter-react-16";

import MyContext, { useMyContext } from "./MyContext";
import * as MyContextModule from "./MyContext";

Enzyme.configure({ adapter: new Adapter() });

export const MyComponent = () => {
  const { myVal } = useMyContext(); // instead of useContext(MyContext)

  return <div data-test="my-component">{myVal}</div>;
};

it("renders the correct text", () => {
  jest.spyOn(MyContextModule, "useMyContext").mockImplementation(() => ({
    myVal: "foobar"
  }));

  const wrapper = shallow(
    <MyContext.Provider>
      <MyComponent />
    </MyContext.Provider>
  ).dive();
  expect(wrapper.text()).toEqual("foobar");
});

It’d be because .dive() needs to access the context in order to forward it along. However, if you try this, it might work:

it("renders the correct text", () => {
  const wrapper = shallow(
    <MyComponent />,
    {
      wrappingComponent: MyContext.Provider,
      wrappingComponentProps: { value: { myVal: 'foobar' }
    }
  );
  expect(wrapper.text()).toEqual("foobar"); // expected "foobar" received ""
});