In the first part of this series I looked at why mocking is useful.
In this part Iâll cover the basic format of React mock components.
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 look again at the components weâre working with: BlogPage
and PostContent
.
Hereâs BlogPage
:
const getPostIdFromUrl = url =>
url.substr(url.lastIndexOf("/") + 1)
export const BlogPage = ({ url }) => {
const id = getPostIdFromUrl(url)
return (
<PostContent id={id} />
)
}
BlogPage
doesnât do much other than show a PostContent
. But it does have a little piece of functionality that weâre interested in, which is parsing the url
prop value to pull out the required post id
.
PostContent
is a little more complicated: it calls the browserâs in-built fetch
function to retrieve the text of a blog post at the URL /post?id=${id}
, where id
is a prop passed to it.
export const PostContent = ({ id }) => {
const [ text, setText ] = useState("")
useEffect(() => {
fetchPostContent(id)
}, [id])
const fetchPostContent = async () => {
const result = await fetch(`/post?id=${id}`)
if (result.ok) {
setText(await result.text())
}
}
return <p>{text}</p>
}
Actually, what PostContent
does isnât important because weâre not going to look at it again!
Weâre going to write some tests for BlogPage
in our test file BlogPage.test.js
. To do that, weâll mock out PostContent
so that we wonât have to worry about its implementation.
The important point is that we stub out PostContent
so that our BlogPage.test.js
test suite is shielded from whatever it is that PostContent
does.
Hereâs the mock for PostContent
:
import { PostContent } from "../src/PostContent"
jest.mock("../src/PostContent", () => ({
PostContent: jest.fn(() => (
<div data-testid="PostContent" />
))
}))
Letâs break this down.
- The mock is defined with
jest.mock
. This must mirror the corresponding import. The call is hoisted so that theimport
can be replaced. Jest replaces the entire module with your newly defined module. So in this case, weâre mocking out the entire../src/PostContent
file. - Since mocks are at the module level, any component youâre mocking will need to be in its own module.
- The call to
jest.fn
produces a spy: an object that records when it is called and with what parameters. We can then test calls using thetoHaveBeenCalled
andtoHaveBeenCalledWith
matchers. - The parameter to
jest.fn
defines a stub value which is returned when the function is called (when the component is rendered). - Stub implementations should always be as simple as you can make them. For React components, that means a
div
âwhich is arguably the HTML element with the least amount of meaning! - It does have an attribute of
data-testid
that weâll use to get hold of this specific element in the DOM. - React Testing Library argues against using
data-testid
where possible, because it wants you to treat your testing as if the test runner was a real person using your software. But for mocks I ignore that guidance, because mocks are by definition a technical concern. - The
data-testid
value matches the name of component. In this case that means itâsPostContent
. This is a standard convention that I follow for all my mocks.
This is the basic form of React component mocks. 90% (or more) of my mocks look this. The other 10% have some small additions that weâll look at in later posts.
With that mock in place, letâs write some tests for BlogPage
.
Verifying that the mocked component is rendered in the DOM
describe("BlogPage", () => {
it("renders a PostContent", () => {
render(<BlogPage url="http://example.com/blog/my-web-page" />)
expect(screen.queryByTestId("PostContent"))
.toBeInTheDocument()
})
})
This test is the first of two tests that are always required when you use component mocks. The screen.queryByTestId
searches in the current DOM for a component with a data-testid
value of PostContent
.
In other words, it checks that we did in fact render the PostContent
component.
The responsible use of queryByTestId
Notice that Iâve used queryByTestId
. React Testing Library tries to push you away from this function on two accounts: first, it wants you to use getBy
in favour of queryBy
, and second, as Iâve already mentioned above, it doesnât want you to search by test ID.
In fact, testing mocks is about the only time I use queryByTestId
. I canât think of a time that Iâve not managed to avoid using TestId
variants for non-mocked components. But for mocks, its perfect: because itâs exactly that technical detail that we want to check. The user will never see this component, itâs purely there for our tests.
What we gain is the ability to have a consistent way of building mock objects: <div data-testid="ComponentName" />
is the standard pattern we can use for all mock objects.
getBy*
vs queryBy*
getBy
variants raise exceptions if they canât match an element. In my opinion, this is only appropriate when the calls are not part of an expectation.
So if you had:
expect(screen.getByTestId("PostContent"))
.toBeInTheDocument()
If you hadnât rendered <PostContent />
this test would blow up with an exception from getByTestId
. The expectation is never run at all!
Given the choice between an expectation failing and an exception being raised, Iâll choose the expectation any day, since itâs more meaningful to the test runner.
Unit tests, and in particular when TDD style tests, are very often about the presence of elements. For these tests I find the
queryBy
much more to my liking.
Verifying that the mock is passed the correct props
The second test we need checks that the right props were passed to PostContent
.
it("constructs a PostContent with an id prop created from the url", () => {
const postId = "my-amazing-post"
render(<BlogPage url={`http://example.com/blog/${postId}`} />)
expect(PostContent).toHaveBeenCalledWith(
{ id: postId },
expect.anything())
})
This uses the standard Jest matchers, toHaveBeenCalledWith
to ensure that the PostContent
function was called with the parameters weâre expecting.
When React instantiates your component, itâs simply calling the defined function with props as an object as the first parameter, and a ref as the second parameter. The second parameter is usually unimportant.
The JSX statement <PostContent id="my-amazing-post" />
results in the function call PostContent({ id: "my-amazing-post" })
.
However, it also includes a phantom second parameter that is never useful to us, so we have to account for that.
Using expect.anything
for the second parameter to toHaveBeenCalledWith
The second parameter that React passes to your component is an instance ref. Itâs usually unimportant to our tests, so youâll always want to pass expect.anything()
to signify that you arenât interested in its value.
If you wanted to get rid of the expect.anything()
call, you could write your own Jest matcher that passes it for you.
If youâre passing no props, just use toHaveBeenCalled
On rare occasions the component youâve mocked will take no parameters. You can use toHaveBeenCalled
as a simpler version of toHaveBeenCalledWith
.
Understanding the basic rules of component mocks
Weâve written two tests and one mock. Hereâs the important lessons that weâve uncovered so far:
- Your mock should be a spy using
jest.fn
and have a stub return value of the simplest component you can possibly have, which is<div />
- You should also set a
data-testid
attribute so you can directly pinpoint this element in the DOM. - The value of this attribute is, by convention, the name of the mocked component. So for the
PostContent
component, its stubbed value is<div data-testid="PostContent" />
. - Every mock requires at least two tests: the first checks that it is present in the DOM, and the second tests that it was called with the correct props.
Why two tests?
Iâve mentioned a couple of times that we need at least two tests. But why is this?
If you didn't have the first test, to check for presence in the DOM, then you could make the second test pass by using a simple function call:
export const BlogPost = () => {
PostContent({ id: "my-awesome-post" })
return null
}
Why you would want to do this is a subject of a whole other blog post, but hereâs the short version: generally we consider a function call to be simpler than a JSX statement. When youâre using strict test principles you should always write the simplest code to make your test pass.
Now what about if you had the first test, but not the second?
You could make it pass like this:
export const BlogPost = () => (
<PostContent />
)
Again, this is the simplest production code to make the test pass.
In order to get to the actual solution, you need both tests.
This is an important difference between end-to-end tests and unit tests: unit tests are defensive in a way that end-to-end tests tend not to be.
Key point: Always write the simplest production code to make your tests pass. Doing so will help you write a test suite which covers all scenarios.
That covers the basics of mock components. In the next part, weâll look at testing child components that are passed to your mocks.