Testing multiple instances of the same mocked component

Daniel Irvine 🏳️‍🌈 - Sep 7 '20 - - Dev Community

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.

GitHub logo 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>
);


Enter fullscreen mode Exit fullscreen mode

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)
  })
})


Enter fullscreen mode Exit fullscreen mode

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())
})


Enter fullscreen mode Exit fullscreen mode

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"
  ])
})


Enter fullscreen mode Exit fullscreen mode

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
}


Enter fullscreen mode Exit fullscreen mode

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>
  ))
}))


Enter fullscreen mode Exit fullscreen mode

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.

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