
Mocks - don't bite!
They are designed to help you create simpler, more reliable tests. In this series of articles, I will show you the patterns that I rely on when mocking (or βstubbingβ) React components.
Here's a good example of a component stub. I use
jest.mock
, which we'll look at in more detail below.
jest.mock("../src/PostContent", () => ({
PostContent: jest.fn(() => (
<div data-testid="PostContent" />
))
}))
A typical React component stub shouldn't look more complicated. Note the very simple stub value (
div
) and an attribute data-testid
that makes it very easy for us to find a rendered instance in the DOM. By convention, the test identifier used must match the component name. In this case, it is PostContent
.
Before we look at how they are used, let's first remember what mocks are and why you might even want to use them.
What is a mock?
In the JavaScript world, the term mock is very widely used to refer to any mocked implementation or test double . Mocked implementations are simply values ββthat replace others in your production code while the tests are running. They try on the interface of the object being replaced, so the rest of your code works as if there was no replacement.
There are several different reasons why you might want to do this; we'll look at them with examples.
If you are interested in learning more about mocked implementations, read Martin Fowler 's book Mocks Are Not Stubs .
Jest and mocking
Jest has a feature
jest.mock
that lets you mock entire modules that you replace. In this tutorial, I focused on this feature, although there are other ways to replace objects in JavaScript.
In the book Mastering React Test-Driven Development, I use ES6 named module imports to create test doubles. This approach gives a little more flexibility, but looks a little more hackish.
Jest jest.mock
it says mocks ensure your tests are fast and not brittle .
While this is true, this is not the main reason I use mocks.
I use mocks because they help me keep my tests independent of each other.
To understand why this is the case, let's look at an example.
Why moki?
Below is a listing of a component
BlogPage
that does two things: it fetches id
from a property url
and then displays 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 that you are writing tests for this component, and all your tests are included in
BlogPage.test.js
, which is a single test suite that covers components BlogPage
and PostContent
.
At this point, you don't need the mocks yet: we haven't seen
PostContent
it yet, but given the size BlogPage
, there really isn't a need to have two separate test suites, since BlogPage
it's generally simple PostContent
.
Now imagine adding functionality to
BlogPage
both y and y PostContent
. As a gifted developer, you add more and more features every day.
It becomes more difficult to maintain tests in working order. Each new test has a more complex setup, and the test suite starts eating up more of your time β a burden that now needs to be supported.
This is a common problem and I see it all the time in React codebases. Test suites in which even the smallest change will result in many tests failing.
One solution is to split test suites. We will leave
BlogPage.test.js
and create a new PostContent.test.js
one that should contain tests exclusively for PostContent
. The basic idea is that any functions placed in PostContent
must be in PostContent.test.js
, and any functions placed in BlogPage
(such as URL parsing) must be in BlogPage.test.js
.
Okay.
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
BlogPage.test.js
must be aware of the side effects and be prepared to handle them. For example, he will have to keep an fetch
answer ready .
The dependency that we tried to avoid by splitting our test suites still exists.
The organization of our testing has certainly gotten better, but in the end, nothing happened to make our tests less fragile.
For this we need a stub (or mock)
PostContent
.
And in the next part we'll look at how to do this.
Is it really necessary?
By the way, a few words about moving from end-to-end testing to unit tests.
Having test doubles is a key indicator that you are writing unit tests.
Many seasoned testers start new projects right away with unit tests (and mocks) because they know that as their codebase grows, they will run into test instability issues.
Unit tests are usually much smaller than end-to-end tests. They can be so small that they often take no more than three or four lines of code. This makes them great candidates for social coding practices such as pair and ensemble programming.
Even when we do unit testing, test doubles are not always necessary - they are just another tool in your suite that you need to know when and how to apply.
In the next part, we will cover the basic mocking techniques .
