reactjsjestjsreact-hooksreact-test-renderer

How to test a react component that is dependent on useContext hook?


I have a component that uses useContext and then its output is dependent on the value in the context. A simple example:

import React, { useContext } from 'react';

const MyComponent = () => {
  const name = useContext(NameContext);

  return <div>{name}</div>;
};

When testing this component with the shallow renderer from react and jest snapshots. How can I change the value of NameContext?


Solution

  • In general, using hooks shouldn't change testing strategy much. The bigger issue here actually isn't the hook, but the use of context, which complicates things a bit.

    There's a number of ways to make this work, but only approach I've found that works with 'react-test-renderer/shallow' is to inject a mock hook:

    import ShallowRenderer from 'react-test-renderer/shallow';
    
    let realUseContext;
    let useContextMock;
    // Setup mock
    beforeEach(() => {
        realUseContext = React.useContext;
        useContextMock = React.useContext = jest.fn();
    });
    // Cleanup mock
    afterEach(() => {
        React.useContext = realUseContext;
    });
    
    test("mock hook", () => {
        useContextMock.mockReturnValue("Test Value");
        const element = new ShallowRenderer().render(
            <MyComponent />
        );
        expect(element.props.children).toBe('Test Value');
    });
    

    This is a bit dirty, though, and implementation-specific, so if you're able to compromise on the use of the shallow renderer, there's a few other options available:

    Non-shallow render

    If you're not shallow rendering, you can just wrap the component in a context provider to inject the value you want:

    import TestRenderer from 'react-test-renderer';
    
    test("non-shallow render", () => {
        const element = new TestRenderer.create(
            <NameContext.Provider value="Provided Value">
                <MyComponent />
            </NameContext.Provider>
        );
        expect(element.root.findByType("div").children).toEqual(['Provided Value']);
    });
    

    (Disclaimer: this should work, but when I test it, I'm hitting an error which I think is an issue in my setup)

    Shallow render with Enzyme and Dive

    As @skyboyer commented, enzyme's shallow renderer supports .dive allowing you to deeply renderer a part of an otherwise shallow rendered component:

    import { shallow } from "./enzyme";
    
    test("enzyme dive", () => {
        const TestComponent = () => (
            <NameContext.Provider value="Provided Value">
                <MyComponent />
            </NameContext.Provider>
        );
        const element = shallow(<TestComponent />);
        expect(element.find(MyComponent).dive().text()).toBe("Provided Value");
    });
    

    Use ReactDOM

    Finally, the Hooks FAQ has an example of testing hooks with ReactDOM, which works as well. Naturally, using ReactDOM means this is also a deep render, not shallow.

    let container;
    beforeEach(() => {
        container = document.createElement('div');
        document.body.appendChild(container);
    });
    
    afterEach(() => {
        document.body.removeChild(container);
        container = null;
    });
    
    test("with ReactDOM", () => {
        act(() => {
            ReactDOM.render((
                <NameContext.Provider value="Provided Value">
                    <MyComponent />
                </NameContext.Provider>
            ), container);
        });
    
        expect(container.textContent).toBe("Provided Value");
    });