How to test if a mocked component receives the correct props in Jest and RTL (new since React 19)
Peter Jacxsens

Peter Jacxsens @peterlidee

About: Front-end developer

Location:
Belgium
Joined:
May 29, 2022

How to test if a mocked component receives the correct props in Jest and RTL (new since React 19)

Publish Date: May 23
0 0

This article serves 2 purposes. Firstly, explain how to test if a mocked component receives the correct props. Secondly, how this is different since React 19.

tl;dr

expect(User).toHaveBeenCalledWith(
  expect.objectContaining({
    name: 'Peter',
  }),
  undefined // use undefined as second argument
);
Enter fullscreen mode Exit fullscreen mode

Setup

Let's start with a simple example. We make a <Users /> component that takes an array of names.

<Users users={['Peter', 'John']} />
Enter fullscreen mode Exit fullscreen mode

This is the <Users /> component:

import { User } from './User';

export function Users({ users }) {
  return (
    <>
      <h1>Users</h1>
      <ul>
        {users.map((user, i) => (
          <User key={i} name={user} />
        ))}
      </ul>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

and this is the <User /> component:

export function User({ name }) {
  return <li>{name}</li>;
}
Enter fullscreen mode Exit fullscreen mode

Testing

Our goal is to write a unit test for <Users />. We want to test <Users /> in isolation. To do this, we need to mock <User />. This is our initial test file:

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

jest.mock('../User');

const users = ['Peter', 'John'];

describe('<Users />', () => {
  // passes
  test('It renders', () => {
    render(<Users users={users} />);
    const heading = screen.getByRole('heading', { level: 1 });
    expect(heading).toBeInTheDocument();
    expect(heading).toHaveTextContent(/Users/);
    expect(User).toHaveBeenCalledTimes(2);
  });
});
Enter fullscreen mode Exit fullscreen mode

We setup <Users />: render(<Users users={users} />); and test if <h1>Users</h1> is present. We already did an automatic mock of <User /> and verified if got called twice: expect(User).toHaveBeenCalledTimes(2);. This all works and passes.

testing props

Now, we want to check if <User /> was called with the correct props. We expect <User /> to have been called twice. (we already verified this). We expect <User /> to have been called the first time with prop "Peter": <User key={0} name={"Peter"} /> and the second time with prop "John": <User key={1} name={"John"} />. But, how do we do this? Here is a new test:

test('It calls <User /> with the correct props', () => {
  render(<Users users={users} />);
  expect(User).toHaveBeenCalledTimes(2);

  // fails
  expect(User).toHaveBeenNthCalledWith(1, {
    name: 'Peter',
  });
});
Enter fullscreen mode Exit fullscreen mode

We expect the <User /> mock to have been called the first name (nth is 1) with an object: { name: 'Peter' }. At first glance this seems like it should work. But it doesn't. Here is the error message:

*edited

n: 1
  Expected: {"name": "Peter"}
  Received
            {"name": "Peter"},
          + undefined,
Enter fullscreen mode Exit fullscreen mode

What this tells us is that there is a second argument: + undefined that is missing, undefined. Before explaining, let's add that:

// passes
expect(User).toHaveBeenNthCalledWith(
  1,
  {
    name: 'Peter',
  },
  undefined
);
Enter fullscreen mode Exit fullscreen mode

And it works. But, what is undefined? What is this second argument? To be honest, I'm not quite sure. On top of that, before React 19, the second argument wasn't undefined but it was an empty object: {}.

new in React 19

Before React 19, I would've written the above test differently:

// passes before react 19
// fails in react 19
expect(User).toHaveBeenNthCalledWith(
  1,
  {
    name: 'Peter',
  },
  {}
);
Enter fullscreen mode Exit fullscreen mode

or with some more flexibility:

// passes before react 19
// fails in react 19
expect(User).toHaveBeenNthCalledWith(
  1,
  {
    name: 'Peter',
  },
  expect.anything()
);
Enter fullscreen mode Exit fullscreen mode

expect.anything() matches with everything except null or undefined.

But what is this second argument? It seems to be some legacy argument that refers to context. But React has long since reworked context and doesn't use the second argument anymore ... but still expects something there. Before React 19, it expected {}.

With React 19 the React team reworked this and the second argument now needs to be undefined. And that is all I know about it.

A last improvement

This is our test in React 19:

// passes
expect(User).toHaveBeenNthCalledWith(
  1,
  {
    name: 'Peter',
  },
  undefined
);
Enter fullscreen mode Exit fullscreen mode

We explicitly use undefined because expect.anything() does not match undefined.

There is one more improvement to be made. In our case we only have one prop for <User />, name. But suppose there are different props and we don't need them all then this pattern would fail. Therefore, it's good practice to use expect.objectContaining:

expect(User).toHaveBeenNthCalledWith(
  1,
  expect.objectContaining({
    name: 'Peter',
  }),
  undefined
);
Enter fullscreen mode Exit fullscreen mode

This will pass, even if there are other properties.

Conclusion

And that is all. A quick explanation of how to test if a mocked component got called with the correct props, followed by a vague explaination about a second argument that was reworked in React 19. For closure, here is the full test file:

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

jest.mock('../User');

const users = ['Peter', 'John'];

describe('<Users />', () => {
  test('It renders', () => {
    render(<Users users={users} />);
    const heading = screen.getByRole('heading', { level: 1 });
    expect(heading).toBeInTheDocument();
    expect(heading).toHaveTextContent(/Users/);
    expect(User).toHaveBeenCalledTimes(2);
  });
  test('It calls <User /> with the correct props', () => {
    render(<Users users={users} />);
    expect(User).toHaveBeenCalledTimes(2);

    // fails
    /*expect(User).toHaveBeenNthCalledWith(
      1,
      expect.objectContaining({
        name: 'Peter',
      }),
    );*/

    //fails
    /*expect(User).toHaveBeenNthCalledWith(
      1,
      expect.objectContaining({
        name: 'Peter',
      }),
      expect.anything()
    );*/

    // works
    expect(User).toHaveBeenNthCalledWith(
      1,
      expect.objectContaining({
        name: 'Peter',
      }),
      undefined
    );
    expect(User).toHaveBeenNthCalledWith(
      2,
      expect.objectContaining({
        name: 'John',
      }),
      undefined
    );
  });
});
Enter fullscreen mode Exit fullscreen mode

Comments 0 total

    Add comment