reactjsjestjs

How do I mock a child component and call it's props within a test?


I have a React component like this...

function ParentComponent() {
const someLogicA = () => {
  something;
}
  return <>
    <ChildComponent doSomething={() => {someLogicA()}/>
  </>
}

I want to test that when ChildComponent calls the prop doSomething that it triggers the correct logic in the ParentComponent for someLogicA.

The logic of ChildComponent is a bit complex, and I can't just use React Testing Library to grab a text from the screen and click/trigger it. I want to mock ChildComponent directly and call doSomething directly, simulating that ChildComponent actually called it.

So in the end the test would be like...

test('it calls someLogicA when doSomething is called', () => {
  mockChildComponent.doSomething();
  expect(the logic for someLogic to work);
});

Solution

  • This is possible if you mock ChildComponent with a fake component implementation that simply binds a mock function to props.doSomething() inside of its render.

    That mock function can then also be statically exposed on the component function itself, so you can trigger it from the test.

    The below assumes ChildComponent is default exported in its own file as this is not mentioned in the question. However, adjustments can be made to work with a named export pattern, too.

    It does, however, require that the ChildComponent be in a different file than the ParentComponent. If they are in the same file, you cannot mock the ChildComponent before the ESM module initialises and relevant components within have already grabbed internal reference to the real impls within the same module -- at least in a way that doesn't require esoteric hacks. You can also choose to just make the code more testable to get around this if needed. For example, allowing a custom ChildComponent to be passed to ParentComponent, or the render props pattern.

    import React from 'react'
    import { render } from '@testing-library/react'
    import ChildComponent from './ChildComponent'
    import ParentComponent from './ParentComponent'
    
    jest.mock('./ChildComponent', () => {
      const callDoSomething = jest.fn()
      const MockComponent = ({ doSomething }) => {
        // Store the prop to be accessible for testing
        callDoSomething.mockImplementation(() => doSomething())
    
        return null
      }
    
      MockComponent.callDoSomething = callDoSomething
    
      return {
        __esModule: true,
        default: MockComponent,
      }
    })
    
    // ...
    
    it('calls someLogicA when doSomething is called', () => {
      render(<ParentComponent />)
      ChildComponent.callDoSomething()
      // expect(the logic for someLogic to work);
    })
    
    

    It's perhaps worth noting that this rails against usual expectations around "react-testing-library", which encourages tests that assert on tangible user-visible elements and doesn't go out of its way to support tests of underlying implementations.

    It appears you are aware of this in this case and are looking for an escape hatch because rendering the "real thing" is likely hard in your case.

    However, before using this particular escape hatch, you should consider if a more realistic mock could be implemented, if JSDOM can be patched up to work with what you need, or even possibly consider testing with a real browser like Cypress/Playwright component testing, for example.

    You could also consider encapsulating this logic in a hook and using that inside ParentComponent. You could then test this hook in isolation without mocking. This is arguably making your code more testable, which is rarely a bad thing.