Mocks aren’t evil!
They can help you build simpler, more resilient tests. In this series I’ll show you the patterns I use when writing React component mocks.
Here’s a quick example of a component mock. I’m using jest.mock
here, which we’ll look at in more detail below.
jest.mock("../src/PostContent", () => ({
PostContent: jest.fn(() => (
<div data-testid="PostContent" />
))
}))
React component mocks don’t get much more complicated than this. The important thing is that it has a very simple stub value (the div
) and a data-testid
attribute that allows us to find the rendered instance very easily in the DOM. By convention, the test ID used is always the same as the component name. In this case, that’s PostContent
.
Before we look at how this is used, let’s just recap what mocks are and why you might want to use them.
What’s a mock?
In the JavaScript world, the term mock is very loosely applied to mean any test double. Test doubles are simply values that replace others in your production code while tests run. They take on the interface of the object they’re replacing so that the rest of your code operates as if it hadn’t been replaced.
There are a few different reasons why you’d want to do this; we’ll cover them in examples.
If you’re curious about test doubles in general then I suggest reading Martin Fowler’s Mocks Aren't Stubs.
Jest and mocking
Jest has a function called jest.mock
that allows you to mock out entire modules that you’re replacing. This is what I’m using in this guide, although there are other ways of replacing objects in JavaScript.
In Mastering React Test-Driven Development I use ES6 named module imports to create test doubles. That approaches gives slightly more flexibility but feels slightly more hacky.
The Jest page on jest.mock
says that mocks ensure your tests are fast and not flaky.
While that’s true, it’s not the primary reason why I use mocks.
I use mocks because they help me keep my tests independent of each other.
To see why that is, let’s look at an example.
Why mocks?
Below is the listing for the BlogPage
component, which has two jobs: it pulls an id
out of a url
prop and it then renders a PostContent
component with that id
.
const getPostIdFromUrl = url =>
url.substr(url.lastIndexOf("/") + 1)
export const BlogPage = ({ url }) => {
const id = getPostIdFromUrl(url)
return (
<PostContent id={id} />
)
}
Imagine you’ve been writing tests for this component, and all of your tests go into BlogPage.test.js
, which is a single test suite that covers both the BlogPage
and the PostContent
components.
At this stage you have no need for mocks: we haven’t seen PostContent
yet, but given the size of BlogPage
there’s really no need to have two separate test suites, since BlogPage
is mostly just PostContent
.
To stretch your imagination further, now pretend that both BlogPage
and PostContent
grow in functionality. You, being the gifted developer that you are, are adding more and more features each day.
Keeping your tests in a working state is starting to prove difficult. Each new test has more elaborate set up, and the test suite is becoming a time sink. It’s a burden to maintain.
This is a common problem, and I see it all the time with React codebases. Test suites where even the simplest change causes many tests to break.
One solution to this is to split the test suites. We’ll keep BlogPage.test.js
and create a new PostContent.test.js
, which should house tests specifically for behavior in PostContent
. The basic idea is that any functionality that’s housed in PostContent
should be specified in PostContent.test.js
, and any functionality that’s housed in BlogPage
(like the URL parsing) should be in BlogPage.test.js
.
Fine.
But what if rendering PostContent
has side effects?
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>
};
The test suite in BlogPage.test.js
needs to be aware of the side effects and be ready to handle them. For example, it will need to have a fetch
response lined up and waiting.
The dependency that we’ve tried to remove by splitting our test suites is still there.
Our test organization is better for sure, but nothing’s ultimately changed that causes our tests to be less brittle.
For that we need to stub out (or mock) PostContent
.
In the next part we’ll look at how to do that.
Is this really necessary?
By the way, this is when you move from the realm of end-to-end tests and into the realm of unit tests.
The presence of test doubles is a key indicator that you’re writing unit tests.
Many experienced testers will immediately start new projects with unit tests (and mocks) because they know that as their codebase grows they’ll face this problem of test brittleness.
Unit tests tend to be much smaller than end-to-end tests. So small that they’re often not much more than three or four lines of code. That makes them excellent candidates for social coding practices like pair and ensemble programming.
Even when we’re unit testing, test doubles aren’t always necessary—it’s just another tool in your toolbox that you should know when and how to apply.
In the next part, I’ll look at basic mocking techniques.