0

I have a very simple class based component. Which looks like the following:

class MyComponent extends React.Component {
    onPressButton () {
        console.warn('button pressed')
        const { contextFunction } = this.context
        contextFunction()
    }

    render () {
        return (
            <div>
                My Component
                <button onClick={() => onPressButton()}>Press button</button>
            </div>
        )
    }
}


MyComponent.contextType = SomeContext

That is all fine and well and works as expected. However, I am having trouble adding unit tests with jest and enzyme. My current code looks as follows:

test('should render test Press button', async () => {
    const contextFunctionMock = jest.fn()
    const wrapper = shallow(<MyComponent {...props} />)
    wrapper.instance().context = { contextFunction: contextFunctionMock }

    console.log('wrapper.instance()', wrapper.instance())

    await wrapper.instance().onPressButton() // this works just fine
    expect(contextFunctionMock).toHaveBeenCalled() // this errors, basically because ti complains contextFunction is not a function
})

As you can see above, I console.logged my wrapper.instance() to see what is going on. Interestingly enough, the context on the root of the instance object is indeed what I expected it to be based on setting the context, which is something like:

context: {
        contextFunction: [Function: mockConstructor] {
          _isMockFunction: true,
          getMockImplementation: [Function (anonymous)],
          [...Other mock function properties]
        }
...

However, there is a second context, which is in the updater property of the wrapper.instance(), and it is an empty object. Basically looks like the following:

updater: <ref *2> Updater {
        _renderer: ReactShallowRenderer {
          _context: {},
          ...
        }

Not exactly sure if this is the context being used for my component's unit test, but it is currently just an empty object, which makes me think this may be the one being used for it.

Anyway, how can I properly mock my context functions to run on this particular unit tests? Also, why is this happening but does not happen in others with a similar set of circumstances?

3
  • could you provide the react, enzyme and enzyme adapter versions your are using? Commented Jun 24, 2021 at 16:02
  • @diedu react v16.13.1; enzyme v3.11.0; enzyme-adapter-react-16 v1.15.6 Commented Jun 24, 2021 at 18:28
  • Hi there, just checking if any of our answers worked for you, or you rather need another approach Commented Jun 29, 2021 at 5:06

2 Answers 2

1
+50

Problem

A fundamental problem with your code above is that there's no way to assert that the context function is successfully/failing to be called. Right now, you're clicking a button, but there isn't any indication on what's happening after the button is clicked (nothing is being changed/updated within the context/component to reflect any sort of UI change). So asserting that a contextual function is called won't be beneficial if there's no result of clicking the button.

In addition to the above, the enzyme-adapter doesn't support context that uses the createContext method.

However, there's a work-around for this limitation! Instead of unit testing the component, you'll want to create an integration test with the context. Instead of asserting that a contextual function was called, you'll make assertions against the result of clicking on the button that changes context and how it affects the component.

Solution

Since the component is tied to what's in context, you'll create an integration test. For example, you'll wrap the component with context in your test and make assertions against the result:

import * as React from "react";
import { mount } from "enzyme";
import Component from "./path/to/Component";
import ContextProvider from "./path/to/ContextProvider";

const wrapper = mount(
  <ContextProvider>
    <Component /> 
  </ContextProvider>
);

it("updates the UI when the button is clicked", () => {
  wrapper.find("button").simulate("click");

  expect(wrapper.find(...)).toEqual(...);
})

By doing the above, you can make assertions against contextual updates within the Component. In addition, by using mount, you won't have to dive into the ContextProvider to view the Component markup.

Demo Example

This demo utilizes context to toggle a theme from "light" to "dark" and vice versa. Click on the Tests tab to run the App.test.js integration test.

Edit Theme Context Testing

Code Example

App.js

import * as React from "react";
import { ThemeContext } from "./ThemeProvider";
import "./styles.css";

class App extends React.PureComponent {
  render() {
    const { theme, toggleTheme } = this.context;
    return (
      <div className="app">
        <h1>Current Theme</h1>
        <h2 data-testid="theme" className={`${theme}-text`}>
          {theme}
        </h2>
        <button
          className={`${theme}-button button`}
          data-testid="change-theme-button"
          type="button"
          onClick={toggleTheme}
        >
          Change Theme
        </button>
      </div>
    );
  }
}

App.contextType = ThemeContext;

export default App;

ThemeProvider.js

import * as React from "react";

export const ThemeContext = React.createContext();

class ThemeProvider extends React.Component {
  state = {
    theme: "light"
  };

  toggleTheme = () => {
    this.setState((prevState) => ({
      theme: prevState.theme === "light" ? "dark" : "light"
    }));
  };

  render = () => (
    <ThemeContext.Provider
      value={{ theme: this.state.theme, toggleTheme: this.toggleTheme }}
    >
      {this.props.children}
    </ThemeContext.Provider>
  );
}

export default ThemeProvider;

index.js

import * as React from "react";
import ReactDOM from "react-dom";
import ThemeProvider from "./ThemeProvider";
import App from "./App";

ReactDOM.render(
  <React.StrictMode>
    <ThemeProvider>
      <App />
    </ThemeProvider>
  </React.StrictMode>,
  document.getElementById("root")
);

Test Example

An example of how to test against the demo example above.

withTheme.js (an optional reusable testing factory function to wrap a component with context -- especially useful for when you may want to call wrapper.setProps() on the root to update a component's props)

import * as React from "react";
import { mount } from "enzyme";
import ThemeProvider from "./ThemeProvider";

/**
 * Factory function to create a mounted wrapper with context for a React component
 *
 * @param Component - Component to be mounted
 * @param options - Optional options for enzyme's mount function.
 * @function createElement - Creates a wrapper around passed in component with incoming props (now we can use wrapper.setProps on root)
 * @returns ReactWrapper - a mounted React component with context.
 */
export const withTheme = (Component, options = {}) =>
  mount(
    React.createElement((props) => (
      <ThemeProvider>{React.cloneElement(Component, props)}</ThemeProvider>
    )),
    options
  );

export default withTheme;

App.test.js

import * as React from "react";
import { configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import withTheme from "./withTheme";
import App from "./App";

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

// wrapping "App" with some context
const wrapper = withTheme(<App />);

/*
  THIS "findByTestId" FUNCTION IS OPTIONAL! 
 
  I'm using "data-testid" attributes, since they're static properties in 
  the DOM that are easier to find within a "wrapper". 
  This is 100% optional, but easier to use when a "className" may be 
  dynamic -- such as when using css modules that create dynamic class names.
*/
const findByTestId = (id) => wrapper.find(`[data-testid='${id}']`);

describe("App", () => {
  it("initially displays a light theme", () => {
    expect(findByTestId("theme").text()).toEqual("light");
    expect(findByTestId("theme").prop("className")).toEqual("light-text");

    expect(findByTestId("change-theme-button").prop("className")).toContain(
      "light-button"
    );
  });

  it("clicking on the 'Change Theme' button toggles the theme between 'light' and 'dark'", () => {
    // change theme to "dark"
    findByTestId("change-theme-button").simulate("click");

    expect(findByTestId("theme").text()).toEqual("dark");
    expect(findByTestId("theme").prop("className")).toEqual("dark-text");
    expect(findByTestId("change-theme-button").prop("className")).toContain(
      "dark-button"
    );

    // change theme to "light"
    findByTestId("change-theme-button").simulate("click");

    expect(findByTestId("theme").text()).toEqual("light");
  });
});
Sign up to request clarification or add additional context in comments.

Comments

1

As for today, the new context API is not supported by enzyme, the only solution I found is to use this utility https://www.npmjs.com/package/shallow-with-context

import { configure, shallow } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import { withContext } from "shallow-with-context";
import MyComponent from "./MyComponent";

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

describe("Context", () => {
  it("should render test Press button", async () => {
    const contextFunctionMock = jest.fn();
    const context = { contextFunction: contextFunctionMock };
    const MyComponentWithContext = withContext(MyComponent, context);
    const wrapper = shallow(<MyComponentWithContext />, { context });
    await wrapper.instance().onPressButton();
    expect(contextFunctionMock).toHaveBeenCalled();
  });
});

https://codesandbox.io/s/enzyme-context-test-xhfj3?file=/src/MyComponent.test.tsx

2 Comments

That did not work unfortunately. I still have the same problem, but unlike the way I am currently setting it, it does not set the root value of context in the wrapper.instance() object.
@theJuls I updated my answer with another approach

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.