How to test a component passed as prop with Jest

Peter Jacxsens - Jan 6 '23 - - Dev Community

React components can be passed as props. But, they are tricky to test. In this article I will explain how to test components passed as props. Let's start with a simple example.

Example

We have 3 components: Parent, Child and User. These examples are available on github in the components folder.

// components/User.js

export default function User({ name }){
  return(
    <>
      <div>component User</div>
      <div>name: {name}</div>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode
// components/Child.js

export default function Child(props){
  return(
    <>
      <div>component Child</div>
      {props.user}
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode
// components/Parent.js

import Child from './Child'
import User from './User'

export default function Parent(){
  const user = <User name="Peter" />
  return(
    <>
      <div>component Parent</div>
      <Child user={user} />
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

As you can see, the Parent renders Child and passes to Child the User component via the user prop. Child then renders User by calling it: {props.user}.

The result is:

component Parent
component Child
component User
name: Peter
Enter fullscreen mode Exit fullscreen mode

On a sidenote

To setup Jest testing in Next I followed the steps in the documentation. Then I added some customizations:

  • auto include jest-dom in every test
  • auto clear all mocks
  • added eslint for react-testing-library

What do we want to test?

We want to run a test on the Parent.js file because that is where we pass the User component as a prop to Child component.

Here is a quick look at the Parent component again:

// components/Parent.js

import Child from './Child'
import User from './User'

export default function Parent(){
  const user = <User name="Peter" />
  return(
    <>
      <div>component Parent</div>
      <Child user={user} />
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

And here is a list of the things we are going to test:

  1. Parent renders
  2. Child mock got called
  3. User mock got called
  4. User mock got called with prop name
  5. Child mock got called with User mock as prop

1. Parent renders

We test if the Parent renders by testing if the text component Parent is in the screen.

// components/__test__/Parent.test.js

import { render, screen } from '@testing-library/react'
import Parent from '../Parent'

test('1. Parent renders', () => {
  render(<Parent />)
  expect(screen.getByText(/component Parent/i)).toBeInTheDocument()
})
Enter fullscreen mode Exit fullscreen mode

2. Child mock got called

Next we want to test Child. This is a unit test, we are testing the Parent component. We don't want to test Child component so we mock Child and then test if the Child mock was called.

// components/__test__/Parent.test.js

import { render, screen } from '@testing-library/react'
import Parent from '../Parent'
import Child from '../Child'

jest.mock('../Child')

test('2. Child mock was called', () => {
  render(<Parent />)
  expect(Child).toHaveBeenCalled()
})
Enter fullscreen mode Exit fullscreen mode

We imported Child, did an automatic mock on it: jest.mock('../Child') and test if the mock was called.

3. User mock got called

Before we continue let's take a closer look at the Child component.

// components/Child.js

export default function Child(props){
  return(
    <>
      <div>component Child</div>
      {props.user}
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

Our functional component Child takes as it's argument an object: props. The props object has a property: user. The value of this user property is the functional component User.

<Child user={<User name="Peter" />} />
// equals
{Child({ user: <User name="Peter" />})}
Enter fullscreen mode Exit fullscreen mode

Parent renders Child and Child renders User. Child renders User by returning {props.user}. This calls the value of props.user: it calls the functional component User.

But, we mocked Child:

jest.mock('../Child')
Enter fullscreen mode Exit fullscreen mode

Child now no longer refers to the component but to a mocking function: jest.fn(). This Child mock doesn't return anything (it returns undefined).

This means our Child mock won't render User anymore. In our Child component, we return User: {props.user}. In our Child mock, we don't return anything. By mocking Child we stopped rendering User and that is a problem because we want to test if User was called.

Let's test this:

// test fails
test('3. User mock was called', () => {
  render(<Parent />)
  expect(User).toHaveBeenCalled()
})
Enter fullscreen mode Exit fullscreen mode

As expect, this test fails. By mocking Child, User was no longer rendered or called. The problem is that User is no longer called by the Child mock.

The solution is to call or return User from the Child mock. To return something from a mock we can use .mockImplementation. What do we return? props.user because that is where User was passed.

// mock Child
jest.mock('../Child')
// return props.user from Child mock
Child.mockImplementation(props => props.user)
Enter fullscreen mode Exit fullscreen mode

Here is the entire test (it passes now).

// components/__test__/Parent.test.js

import { render, screen } from '@testing-library/react'
import Parent from '../Parent'
import Child from '../Child'
import User from '../User'

jest.mock('../Child')
jest.mock('../User')
Child.mockImplementation(props => props.user)

// test 1
// test 2

// passes
test('3. User mock was called', () => {
  render(<Parent />)
  expect(User).toHaveBeenCalled()
})
Enter fullscreen mode Exit fullscreen mode

And that is all, we now have succesfully tested that both Child and User mocks were called.

4. User mock got called with prop name

In our example, we call User with the prop name="Peter". A fixed value, not usefull to test. But this is just a simple example and I want to demonstrate how to test this prop.

// components/__test__/Parent.test.js

// test passes
test('4. User mock was called with the correct props', () => {
  render(<Parent />)
  expect(User).toHaveBeenCalledWith(
    expect.objectContaining({ name: "Peter" }),
    expect.anything()
  )
})
Enter fullscreen mode Exit fullscreen mode

5. Child mock got called with User mock as prop

Think about this. How do we test if the User mock was passed as a prop to the Child mock? Stop reading and really think about it.

...

Did you get it? It's a trick question. We already have. We proved User mock was passed because:

// passed
expect(User).toHaveBeenCalled()
Enter fullscreen mode Exit fullscreen mode

On a sidenote: There is a dark path here. Look at this test:

// fails
test('Child mock was called with User mock', () => {
  render(<Parent />)
  expect(Child).toHaveBeenCalledWith(
    { user: <User name="Peter" />},
    expect.anything()
  )
})
Enter fullscreen mode Exit fullscreen mode

This test fails and this is what our Jest terminal says:

expect(jest.fn()).toHaveBeenCalledWith(...expected)

  - Expected
  + Received

  - {"user": <User name="Peter" />},
  + {"user": <User name="Peter" />},
    {},
Enter fullscreen mode Exit fullscreen mode

I'm not 100% sure about this but I think you cannot test if Child was called with User as a prop using .toHaveBeenCalledWith(). It could be a referential inequality thing or something else.

The point I'm trying to make here is that using .toHaveBeenCalledWith() is not a valid option here.

Summary

Components can take other components as props. This can make testing tricky.

You can solve this problem by letting your component mocks return the propped component. By returning the propped component it renders (gets called) and becomes available for testing.

The propped component having been called proves that it was correctly passed.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player