DEV Community

loading...

The basic form for React component mocks

d_ir profile image Daniel Irvine 🏳️‍🌈 ・6 min read

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.

GitHub logo 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 the import 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 the toHaveBeenCalled and toHaveBeenCalledWith 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’s PostContent. 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.

Discussion

pic
Editor guide
Collapse
vishal1419 profile image
Vishal Sherathiya

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:

jest.mock('../../components/Table', () => ({
  default: jest.fn(() => <div data-testid="Table" />),
}));
Enter fullscreen mode Exit fullscreen mode

But still I was getting an error, so I used this code:

jest.mock('../../components/Table', () => ({
  __esModule: true,
  default: jest.fn(() => <div data-testid="Table" />),
}));
Enter fullscreen mode Exit fullscreen mode

After that I also had to downgrade react-scripts to 3.4.0 and add a new package:

npm i -D jest-environment-jsdom-sixteen
Enter fullscreen mode Exit fullscreen mode

And last change was to use the newly installed library in package.json. Please see new environment added to scripts:

"scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom-sixteen",
    "eject": "react-scripts eject"
},
Enter fullscreen mode Exit fullscreen mode

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.