This is part four of a series on testing React with component mocks. In part 2 we looked at the basic form of component mocks. In part 3, we added the ability to assert on component children. Now we’ll look at the most complex piece of the puzzle: handling multiple instances of the same mock.
All the code samples for this post are available at the following repo.
dirv / mocking-react-components
An example of how to mock React components
Let’s continue with a new component, TopFivePostsPage
, which perhaps unsurprisingly shows the top five posts.
import { PostContent } from "./PostContent"
export const TopFivePostsPage = () => (
<ol>
<PostContent id="top1" />
<PostContent id="top2" />
<PostContent id="top3" />
<PostContent id="top4" />
<PostContent id="top5" />
</ol>
);
To test that, we use queryAllByTestId
in combination with the toHaveLength
matcher.
describe("BlogPage", () => {
it("renders five PostContent components", () => {
render(<TopFivePostsPage />)
expect(screen.queryAllByTestId("PostContent"))
.toHaveLength(5)
})
})
And for our second test, we can use five expect
statements, each with the different prop values.
it("constructs a PostContent for each top 5 entry", () => {
render(<TopFivePostsPage />)
expect(PostContent).toHaveBeenCalledWith(
{ id: "top1" }, expect.anything())
expect(PostContent).toHaveBeenCalledWith(
{ id: "top2" }, expect.anything())
expect(PostContent).toHaveBeenCalledWith(
{ id: "top3" }, expect.anything())
expect(PostContent).toHaveBeenCalledWith(
{ id: "top4" }, expect.anything())
expect(PostContent).toHaveBeenCalledWith(
{ id: "top5" }, expect.anything())
})
But there’s something not quite right about this. We haven’t tested the order of rendering. The toHaveBeenCalledWith
matcher doesn’t care about order.
We can use .mock.calls
instead.
it("renders PostContent items in the right order", () => {
render(<TopFivePostsPage />)
const postContentIds = PostContent.mock.calls.map(
args => args[0].id)
expect(postContentIds).toEqual([
"top1", "top2", "top3", "top4", "top5"
])
})
If you try running this after the first two tests for TopFivePostsPage
, you’ll get a strange error that PostContent
was actually called fifteen times! That’s because when we need to clear our mock between each test.
We do that by adding the clearMocks
property to our Jest config. Here’s my package.json
for comparison.
"jest": {
"transform": {
"^.+\\.jsx?$": "babel-jest"
},
"setupFilesAfterEnv": ["./jest.setup.js"],
"clearMocks": true
}
Notice the last test we wrote actually makes the previous test redundant, so you can delete that one safely.
When that’s not enough: mock instance IDs
Very occasionally, you'll need more than this. For example, if you need to test children passed and you also have multiple instances. In that case, you can use one of the component’s props to give a unique test ID to your component instance.
jest.mock("../src/PostContent", () => ({
PostContent: jest.fn(({ children, id }) => (
<div data-testid={`PostContent-${id}`}>
{children}
</div>
))
}))
Personally, I really dislike this. It’s complex, and more complex than I’m comfortable with. But it exists, and sometimes it’s necessary to use it.
Remember that mocks are there to help you speed up your testing, and testing is there to help speed up your development. When mocks become overly complex, you have to spend more time reading them and maintaining them, so they slow you down. I’ll cover more on this in the next part.
Yet more lessons
So what have we learned now?
- Use
queryAllByTestId
when testing multiple instances of a mocked component - Use
.mock.calls
to check ordering of calls, or for testing render props. - Use Jest’s
clearMocks
configuration setting to ensure your spies are cleared before each test. - If all else fails, you can use props within your rendered output to give unique
data-testid
values for each instance. - Keep your mocks as simple as possible!
That’s all there is to it. In the final part, we’ll look at why mocks can get you into trouble—and how to avoid it.