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
);
Setup
Let's start with a simple example. We make a <Users />
component that takes an array of names.
<Users users={['Peter', 'John']} />
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>
</>
);
}
and this is the <User />
component:
export function User({ name }) {
return <li>{name}</li>;
}
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);
});
});
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',
});
});
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,
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
);
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',
},
{}
);
or with some more flexibility:
// passes before react 19
// fails in react 19
expect(User).toHaveBeenNthCalledWith(
1,
{
name: 'Peter',
},
expect.anything()
);
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
);
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
);
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
);
});
});