Search code examples
reactjsunit-testingjestjs

How to reference an React.Component's Function from a Jest Mock


I have the following (example) component that I am attempting to mock as part of an integration test.

interface MyComponentProps { acceptableRoles: string[]}

export default class MyComponent extends Component<React.PropsWithChildren<MyComponentProps>> {

    public canRender(currentRoles: string[]): boolean {
        // determine if any current roles intersect with acceptableRoles
    }

    public render() {
        return (
            <MyContext.Consumer>
                {(roles) => this.canRender(roles)
                    ? this.props.children
                    : null
                }
            </MyContext.Consumer>
    }
}

I want to mock the output of render() to remove the <MyContext.Consumer> wrapper and passing in the roles into my mock.

When used in the component I'm testing, the above component is simply

<MyComponent acceptableRoles={"role1", "role2"}>
    <div>Show this if allowed</div>
</MyComponent> 

I have tried a number of things and this feels like the closest I can get; however, I receive an error saying that origin.canRender is not a function. When I check it, it is undefined.

const myTestRoles = ["role3", "role4"]; //changes within different tests

jest.mock("./MyComponent", () => (props: any) => {
  const origin = jest.requireActual<MyComponent>("./MyComponent");
  // console.info("Origin: " + origin.canRender); //Outputs as "Origin: undefined"

  return <div>{
            origin.canRender(_user)
            ? props.children
            : null}
          </div>;
  });

Everything "works" until I add a call to the `origin.canRender' which provides the "not a function" error.

I know strict unit testing I would just skip this call with an explicit mock but this got me really wondering if this can be done.

Is it possible to call such an instance method from a mocked method?


Solution

  • Since you are using export default MyComponent, the jest.requireActual('./MyComponent') statement will return { default: [class MyComponent extends Component] }.

    You can access the public method of a JavaScript class by its .prototype property.

    e.g.

    MyComponent.tsx:

    import React, { Component } from 'react';
    
    const MyContext = React.createContext<string[]>([]);
    
    interface MyComponentProps {
      acceptableRoles: string[];
    }
    
    export default class MyComponent extends Component<React.PropsWithChildren<MyComponentProps>> {
      public canRender(currentRoles: string[]): boolean {
        return true;
      }
    
      public render() {
        return <MyContext.Consumer>{(roles) => (this.canRender(roles) ? this.props.children : null)}</MyContext.Consumer>;
      }
    }
    

    index.test.tsx:

    import React from 'react';
    import { render, screen } from '@testing-library/react';
    import MyComponent from './MyComponent';
    
    jest.mock('./MyComponent', () => (props: any) => {
      const origin = jest.requireActual('./MyComponent');
      console.log("🚀 ~ jest.mock ~ origin:", origin)
      console.info('Origin: ', origin.default.prototype.canRender);
    
      return <div>{origin.default.prototype.canRender(['role3']) ? props.children : null}</div>;
    });
    
    describe('78269182', () => {
      test('should pass', () => {
        render(
          <MyComponent acceptableRoles={['role1', 'role2']}>
            <div>Show this if allowed</div>
          </MyComponent>,
        );
        screen.debug();
      });
    });
    

    Test result:

      console.log
        🚀 ~ jest.mock ~ origin: { default: [class MyComponent extends Component] }
    
          at log (stackoverflow/78269182/index.test.tsx:7:11)
    
      console.info
        Origin:  [Function: canRender]
    
          at info (stackoverflow/78269182/index.test.tsx:8:11)
    
      console.log
        <body>
          <div>
            <div>
              <div>
                Show this if allowed
              </div>
            </div>
          </div>
        </body>
    
          at logDOM (node_modules/@testing-library/dom/dist/pretty-dom.js:87:13)
    
     PASS  stackoverflow/78269182/index.test.tsx
      78269182
        √ should pass (40 ms)                                                                                                                                                                                                                                
                                                                                                                                                                                                                                                             
    Test Suites: 1 passed, 1 total                                                                                                                                                                                                                           
    Tests:       1 passed, 1 total                                                                                                                                                                                                                           
    Snapshots:   0 total
    Time:        1.086 s
    Ran all test suites related to changed files.
    

    package versions:

    "jest": "^29.7.0"