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 theimportcan be replaced. Jest replaces the entire module with your newly defined module. So in this case, weāre mocking out the entire../src/PostContentfile. - Since mocks are at the module level, any component youāre mocking will need to be in its own module.
- The call to
jest.fnproduces a spy: an object that records when it is called and with what parameters. We can then test calls using thetoHaveBeenCalledandtoHaveBeenCalledWithmatchers. - The parameter to
jest.fndefines 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-testidthat weāll use to get hold of this specific element in the DOM. - React Testing Library argues against using
data-testidwhere 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-testidvalue 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
queryBymuch 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.fnand have a stub return value of the simplest component you can possibly have, which is<div /> - You should also set a
data-testidattribute 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
PostContentcomponent, 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-scriptsto3.4.0and 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: truecomes from this post.Cannot get this solution to work.
Downgraded
react-scriptsto 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