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.
Top comments (0)