Mocking React context provider in Jest with react-testing-library
Asked Answered
I

2

11

I have a fairly complex context that I wrap around my app to handle authentication and provide the associated data retrieved from the auth service. I'd like to bypass all of the functionality of the provider and just mock a return value. When the context renders, it performs a bunch of initialization functions that I don't want to happen when I test.

I tried something like this in my wrapper function:

const mockValue = {
  error: null,
  isAuthenticated: true,
  currentUser: 'phony',
  login: jest.fn(),
  logout: jest.fn(),
  getAccessToken: jest.fn(),
}

const MockAuthContext = () => ( React.createContext(mockValue) )

jest.mock("../contexts/AuthContext", () => ({
  __esModule: true,
  namedExport: jest.fn(),
  default: jest.fn(),
}));

beforeAll(() => {
  AuthContext.mockImplementation(MockAuthContext);
});

const customRender = (ui, { ...renderOpts } = {}) => {
  const ProviderWrapper = ({ children }) => (
      <AuthContext.Provider>
         {children}
      </AuthContext.Provider>
  );
  return render(ui, { wrapper: ProviderWrapper, ...renderOpts });
};

// re-export everything
export * from "@testing-library/react";

// override render method
export { customRender as render };

Alas, I get an error: Error: Uncaught [Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.

And, indeed, if I log what that Provider renders as, I get:

    {
      '$$typeof': Symbol(react.element),
      type: undefined,
      ...

I've tried setting the provider to AuthContext.getMockImplementation().Provider. No dice.

Anyway, does anyone see what I'm trying to accomplish here? I'd like to just mock the entire context so that the component just gets a provider that returns known values without executing any context code. Is that possible with react-testing-library and Jest?

Ixion answered 9/10, 2020 at 2:4 Comment(2)
What is AuthContext and why do you need to mock it? This may be XY problem. Usually there is no need to mock providers because new values can be passed through them with value.Parthenopaeus
So, I think I wasn't clear that what I'm trying to avoid is the execution of a bunch of code in the provider. When it mounts, a bunch of configuration and checking for authentication data happens. I want to short-circuit all of that when testing such that it just dumbly returns what I give it in value. I think I came up with a decent hack to set a testMode flag prop on the Provider component, such that it just returns value (and the associated mock jest.fn()'s) and ignores the rest of the Provider initialization. Turns out that I needn't mock the context itself if I do that.Ixion
P
7

The error means that AuthContext.Provider is not React component. AuthContext is Jest spy, i.e. a function, and it doesn't have Provider property. Since AuthContext is expected to be React context, it should be defined as such. For default export it should be:

jest.mock("../contexts/AuthContext", () => ({
  __esModule: true,
  default: React.createContext()
}));

Then any mocked value can be provided in tests as:

<AuthContext.Provider value={mockValue}>
Parthenopaeus answered 9/10, 2020 at 16:9 Comment(1)
Ah, much less hacky. Thanks, I'm set, but I'll give that a whirl later for educational purposes. Thanks!Ixion
M
5

For those googling, I found that mocking the context provider was needlessly complex and error prone. Instead, I mocked the context caller. That way the provider, and any functions or configuration called by the provider, is not used.

To explain in more detail, your app is likely wrapped in the provider:

root.render(
  <AuthProvider>
    <App />
  <AuthProvider>
);

And inside your component, you're likely calling the context with a function:

AuthContext = React.createContext();
const yourContext = useContext(AuthContext);

/* or */ 

const useContextWrapper = () => {
  AuthContext = React.createContext()
  return useContext(AuthContext)
};
// then in the component: 
const yourContext = useContextWrapper();

Instead of mocking the provider, which involves mocking a complex react component, it's easier to mock the function calling the provider. In my case, I have two providers (Auth0 and a custom userProfile) that each have a wrapper function around the caller for easy use. Here's the code that I used (sanitized for public display, of course), where "useAuth0" and "useUserProfile" are the functions that provide the context in the component:

// Mock the Auth0 hook and make it return a logged in state
// This way we don't need to mock the context provider
jest.mock('@auth0/auth0-react', () => ({
  useAuth0: () => {
    return {
      isLoading: false,
      user: { sub: "foobar", email: "foo@bar"},
      isAuthenticated: true,
      loginWithRedirect: jest.fn(),
      logout: jest.fn(),
    }
  }
}));

// Mock the UserProfile hook and make it return a user profile
// This way we don't need to mock the context provider
jest.mock('path/to/wrapper/function/probably/near/provider', () => ({
  useUserProfile: () => {
    return {
      userProfile: {
        email: "mock@mock",
      },
    }
}}));
Maximalist answered 27/4, 2023 at 19:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.