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.
Top comments (3)
When using jest.mock with React + Typescript, first test case fails because render method gets an object instead of component.
Solution:
The component in the example is using named Export, but my component was using Default Export.
So, I tried this code:
But still I was getting an error, so I used this code:
After that I also had to downgrade
react-scripts
to3.4.0
and add a new package:And last change was to use the newly installed library in package.json. Please see new environment added to scripts:
After doing this hard work, everything is working fine now.
Side Effects:
In VS Code, Files explorer marks test files red.
Also, tests not showing icon of success or failure in coding area.
But no issues when running the test cases.
Solution to that is preety easy.
Either restart VS Code
Or restart Jest Runner and all the side-effects will be removed.
Credits:
github.com/facebook/jest/issues/97... - suggestions on changing packages
thoughtbot.com/blog/mocking-react-... -
__esModule: true
comes from this post.Cannot get this solution to work.
Downgraded
react-scripts
to 3.4.0, installedjest-environment-jsdom-sixteen
, runningreact-scripts test --env=jsdom-sixteen
, just can't get it to run.Finally! I had:
Which if course, was resetting the implementation. I think the issue is react-scripts 4 added
"resetMocks": true,
as a default so it might be fixed by just:in your package.json