Unit test of component wrapped in react-intl throws `TypeError: _react.default.useContext … return value is not iterable`

I know I need to pass the context to the component under test and I’ve tried a few different ways, but I can’t seem to make it work. Under this setup, I’m getting this error:

    TypeError: _react.default.useContext is not a function or its return value is not iterable

       7 |
       8 | function MyComponent(props) {
    >  9 |   const [locale, setLocale] = React.useContext(LocaleContext);
         |                                     ^

Any advice on what I’m doing wrong? Am I better off switching to enzyme or react-test-renderer?

App.js

import React from "react";
import { IntlProvider } from "react-intl";

import MyComponent from "./components/MyComponent ";

import "./App.css";

import { LocaleContext } from "./LocaleContext";
const messages = { zh: require("./translations/zh") };

function App() {
  const [locale] = React.useContext(LocaleContext);

  return (
    <IntlProvider locale={locale} messages={messages[locale]}>
      <div className="App">
        <MyComponent />
      </div>
    </IntlProvider>
  );
}

export default App;

LocaleContext.js

import React, { useContext } from "react";
export const LocaleContext = React.createContext();
export const useLocaleContext = () => useContext(LocaleContext);

export const LocaleContextProvider = props => {
  const [locale, setLocale] = React.useState("en");
  return (
    <LocaleContext.Provider value={[locale, setLocale]}>
      {props.children}
    </LocaleContext.Provider>
  );
};

MyComponent.js

import React from "react";
import { FormattedMessage } from "react-intl";

import { LocaleContext } from "../LocaleContext";

import logo from "../logo.svg";

function MyComponent(props) {
  const [locale, setLocale] = React.useContext(LocaleContext);
  const nextLocale = locale === "en" ? "zh" : "en";

  return (
    <header className="App-header">
      <img src={logo} className="App-logo" alt="logo" />
      <h1>
        <FormattedMessage id="title" defaultMessage="Hello World!" />
      </h1>
      <h2>
        <FormattedMessage id="subtitle" defaultMessage="Welcome to our app" />
      </h2>
      <button onClick={() => setLocale(nextLocale)}>
        Change language to {nextLocale}
      </button>
    </header>
  );
}

export default MyComponent;

MyComponent.test.js

import React from "react";
import { render } from "@testing-library/react";

import * as LocaleContext from "../LocaleContext";
import MyComponentfrom "./MyComponent";

test("renders `hello world` heading", () => {
  const contextValues = { title: "Hey There" };
  jest
    .spyOn(LocaleContext, "useLocaleContext")
    .mockImplementation(() => contextValues);

  const { getByText } = render(<MyComponent/>);
  const helloWorldText = getByText(/hello world/i);
  expect(helloWorldText).toBeInTheDocument();
});

package.json

{
  "scripts": {
    ...
    "test": "react-scripts test"
  },
  "dependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.3.2",
    "react": "^16.13.1",
    "react-intl": "^4.6.9",
    "react-scripts": "3.4.1"
    ...
  }
  ...
}

Answer

You may want to study the wrapper api, and setup for a custom render, but the gist is you create a test wrapper that provides the context provider for testing.

For example, I use react-intl so for testing I have a test utility intlWrapper

import React from 'react';
import { IntlProvider } from 'react-intl';

export const intlWrapper = ({ children }) => (
  <IntlProvider locale="en">{children}</IntlProvider>
);

And to test a component it is used as such

const {/* query selectors */} = render(
  <ComponentUsingIntl />,
  { wrapper: intlWrapper },
);

To suit your needs I think you should create a wrapper for your LocaleContextProvider

import { LocaleContextProvider } from '../LocaleContext';

export const contextWrapper = ({ children }) => (
  <LocaleContextProvider>{children}</LocaleContextProvider>
);

You can now import your test contextWrapper and use

const { getByText } = render(<MyComponent/>, { wrapper: contextWrapper });