Mocking React components (Jest mocking + React part 2)

Peter Jacxsens - Sep 8 '22 - - Dev Community

In the first part of this series we talked about what a mock is, why you need mocking and how to set up mocks.

In this part, we will be applying the things we learned in part 1 to testing React components. We start of by illustrating why you need mocking and continue with actually mocking some components.

  1. Why you need mocking to test React
  2. How to mock React components
  3. Mocking components that have props

The examples I use in this article are available on github (src/part2). These files a build upon create-react-app so you can run them using npm run start or run the tests using npm run test.

1. Why you need mocking to test React

React components are always linked/nested to other components or packages. But, when testing a component, you want to test that component only. Therefore, you need to somehow isolate it from the rest. This is where Jest mocking comes into play. Here is an example:

// part2/example1/ChildComponent.js
function ChildComponent(){
  return(
    <div className="ChildComponent">
      Child component
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode
// part2/example1/ParentComponent.js
function ParentComponent(){
  return(
    <div className="ParentComponent">
      <div>Parent Component</div>
      <ChildComponent />
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Our goal is to test ParentComponent. With no mocking it would look like this:

// part2/example1/__tests__/test1.js
import { screen, render } from '@testing-library/react'
import ParentComponent from '../ParentComponent'

test('ParentComponent renders', () => {
  render(<ParentComponent />)
  expect(screen.getByText(/Parent Component/i)).toBeInTheDocument()
})
Enter fullscreen mode Exit fullscreen mode

As expected, this test renders both the parent and the child components. We can test this by adding screen.debug() (prints output in console) or by adding another expect statement.

// part2/example1/__tests__/test1.js
import { screen, render } from '@testing-library/react'
import ParentComponent from '../ParentComponent'

test('ParentComponent and ChildComponent render', () => {
  render(<ParentComponent />)
  screen.debug()
  expect(screen.getByText(/Parent Component/i)).toBeInTheDocument()
  expect(screen.getByText(/Child Component/i)).toBeInTheDocument()
})
Enter fullscreen mode Exit fullscreen mode

This is normal react behaviour but not what we want when testing! Let's say somebody else is yet to write that ChildComponent and you don't know anything about it. Maybe the child also contains the text "Parent Component"? This would make your test fail. (getByText returns error when there is more then 1 match)

This was a simplistic example but it makes the problem clear. When testing a component, we don't want interference from other components or modules. Yet, React components are always linked to each other. So how do we isolate a component? With a Jest mock.


2. How to mock React components

Let's rewrite the test. We add one rule jest.mock('../ChildComponent') and we update the expect statement for the child component.

// part2/example1/__tests__/test2.js
import { screen, render } from '@testing-library/react'
import ParentComponent from '../ParentComponent'

jest.mock('../ChildComponent')

test('ParentComponent renders and ChildComponent does not', () => {
  render(<ParentComponent />)
  expect(screen.getByText(/Parent Component/i)).toBeInTheDocument()
  // notice the .not
  expect(screen.queryByText(/Child Component/i)).not.toBeInTheDocument()
})
Enter fullscreen mode Exit fullscreen mode

2.1 Automatic mocks with jest.mock()

Let us now break down this new test. jest.mock() takes as it's first parameter the path to the file the module is in. When there is no second parameter, Jest performs an automatic mock. This means that it sets up a mock of the module(s) you're importing.

For this line: jest.mock('../ChildComponent'), Jest will look at any imports we make from this file, and replace these imports with jest.fn().

The child was mocked. This means that the actual component was replaced by an empty mock. As we saw in the first part of this series, mock function don't return anything (unless we tell them to). This means that the content of the child should no longer be in the document and this is what we tested:

// previous test
expect(screen.getByText(/Child Component/i)).toBeInTheDocument()
// last test: 
// notice the queryByText and the .not method
expect(screen.queryByText(/Child Component/i)).not.toBeInTheDocument()
Enter fullscreen mode Exit fullscreen mode

We succesfully mocked ChildComponent.

On a sidenote: we called jest.mock(). Jest mocking is confusing because it uses the terms mock and mocking to refer to different things. jest.fn() creates a mocking function. Then there is the .mock property on a mocking function that gives you access to the 'logs'. (f.e. mockedFunction.mock.calls[0][0]). And just now we used the .mock method on the jest global object. Sorry about this confusion but that is just how Jest does it.

2.2 Testing the mock

But, how do we access the mock? A mocking function - jest.fn() - is like a 'log'. But how do we access this 'log'?

// part2/example1/__tests__/test3.js
import { render } from '@testing-library/react'
import ParentComponent from '../ParentComponent'
import ChildComponent from '../ChildComponent'

jest.mock('../ChildComponent')

test('ChildComponent mock can be tested', () => {
  render(<ParentComponent />)
  // using .mock property
  expect(ChildComponent.mock.calls).toHaveLength(1)
  // using jest helpers
  expect(ChildComponent).toHaveBeenCalled()
})
Enter fullscreen mode Exit fullscreen mode

Notice that we now imported ChildComponent into our test. When we run the test, the jest.mock does it's automocking thing and we can then access the mocking function (the jest.fn()) by simply calling ChildComponent as we did on the last 2 expect statements:

// using .mock property
expect(ChildComponent.mock.calls).toHaveLength(1)
// using  jest helpers
expect(ChildComponent).toHaveBeenCalled()
Enter fullscreen mode Exit fullscreen mode

ChildComponent now behaves exactly the way we expect a jest.fn() to behave. We can inspect it's .mock property (the 'logs') or we can call the Jest helper methods on it like .toHaveBeenCalled().

2.3 expect(element) vs expect(function)

Be carefull what matcher functions you use when mocking. The matcher .toBeInTheDocument() for example only works on dom elements. A mock is a function. You can only call matchers that expect a function like .toHaveBeenCalled() on a mocking function.

2.4 The final test

A quick recap. To mock a React module we use jest.mock(path). This is called an automatic mock. It automatically mocks the component. You can access this mock by simply calling it's name after you imported it.

This is the definitive test for our ParentComponent:

// part2/example1/__tests__/test4.js
import { screen, render } from '@testing-library/react'
import ParentComponent from '../ParentComponent'
import ChildComponent from '../ChildComponent'

jest.mock('../ChildComponent')

test('ParentComponent rendered', () => {
  render(<ParentComponent />)
  expect(screen.getByText(/Parent Component/i)).toBeInTheDocument()
})

test('ChildComponent mock was called', () => {
  render(<ParentComponent />)
  expect(ChildComponent).toHaveBeenCalled()
})
Enter fullscreen mode Exit fullscreen mode

In case you are wondering why we test the mock: the parent component renders this child. Eventhough we don't want to render the child because we want to test the parent in isolation, we still expect the child to be there. That's why we simulate or mock the child.


3. Mocking components that have props

Until now, we have been working with simplistic example components. What happens when we add a prop to our child?

// part2/example2/ChildComponent.js
function ChildComponent(props){
  return(
    <div className="ChildComponent">
      Child component says {props.message}
    </div>
  )
}
export default ChildComponent
Enter fullscreen mode Exit fullscreen mode
// part2/example2/ParentComponent.js
import ChildComponent from "./ChildComponent"
function ParentComponent(){
  return(
    <div className="ParentComponent">
      <div>Parent Component</div>
      <ChildComponent message="Hello" />
    </div>
  )
}
export default ParentComponent
Enter fullscreen mode Exit fullscreen mode

In the parent we call ChildComponent with a prop message and then the child returns: "Child component says hello".

Again, we want to test the parent while mocking the child. This time we want to test if the mocked child gets called with the correct props. But what does the child get called with? In React, you can call components as functions. These two have the exact same result:

<MyComponent prop1="foo" prop2="bar" />
// equals
{MyComponent({ prop1: "foo", prop2: "bar" })}
Enter fullscreen mode Exit fullscreen mode

From this, it also becomes clear what a component gets called with: an object with all the props. So, we expect the ChildComponent mock to get called with { message: 'hello' }.

3.1 .toHaveBeenCalledWith() on js function mocks

We saw how .toHaveBeenCalledWith() works on an javascript function in part1 of this series but I will quickly refresh this:

function doAThing(callback){
  callback('foo')
}
Enter fullscreen mode Exit fullscreen mode

We have a simple function. It takes a callback function as argument and calls this callback with the argument 'foo'. In a test, we would mock callback and then test if this mock gets called with the correct argument.

test('MockCallBack gets called with the correct argument', () => {
  const mockCallback = jest.fn()
  doSomething(mockCallback)
  expect(mockCallback).toHaveBeenCalledWith('foo')
})
Enter fullscreen mode Exit fullscreen mode

3.2 .toHaveBeenCalledWith() on React component mocks

Now let's use .toHaveBeenCalledWith() to test our ChildComponent mock. We expect it gets called with: { message: 'Hello' }

// part2/example2/__tests__/test1.js
import { render } from '@testing-library/react'
import ParentComponent from '../ParentComponent'
import ChildComponent from '../ChildComponent'

jest.mock('../ChildComponent')

// fails
test('The mocked ChildComponent gets called with the correct props', () => {
  render(<ParentComponent />)
  expect(ChildComponent).toHaveBeenCalledWith(
    { message: 'Hello' }
  )
})
Enter fullscreen mode Exit fullscreen mode

But, this test fails. The reason for this is a bit obscure. When a React component gets called it actually receives two arguments: an object with the props and a ref. I don't fully understand this ref myself but just know that:

  1. This ref value is usually empty ({})
  2. You need it or else your test fails.

So, let's update the test:

// part2/example2/__tests__/test1.js
import { render } from '@testing-library/react'
import ParentComponent from '../ParentComponent'
import ChildComponent from '../ChildComponent'

jest.mock('../ChildComponent')

// passes
test('The mocked ChildComponent gets called with the { message: "Hello" } and {}', () => {
  render(<ParentComponent />)
  expect(ChildComponent).toHaveBeenCalledWith(
    { message: 'Hello' },
    {}
  )
})
Enter fullscreen mode Exit fullscreen mode

Let me quickly recap. We are testing a mock of a React component: <ChildComponent message="Hello" />. We want to test if this mock was called with certain props. But, when a React component gets called it actually receives two arguments: An object with it's props and a second ref value. This second value is usually empty.

The matcher .toHaveBeenCalledWith() receives two things from the mock: the props object and the ref value. We now pass into the matcher what we expect to find: an object with properties (to match the props) and an empty object ´{}´ to match the ref.

This test works but it isn't optimal. What if this ref does have a value for example. To counter this, we replace {} with the following line: expect.anything().

.anything() is a Jest matcher that passes for anything except undefined or null. So, it's an ideal candidate to replace {} or an actual ref value. We update the test:

  expect(ChildComponent).toHaveBeenCalledWith(
    { message: 'Hello' },
    expect.anything()
  )
Enter fullscreen mode Exit fullscreen mode

A second improvement we can make is in the line { message: 'Hello'}}. This performs an exact match. The received object has to exactly match the expected object we pass in. But, this is a not ideal. Let's say that for some reason we only wanted to test one property. How would we do this?

  expect(ChildComponent).toHaveBeenCalledWith(
    expect.objectContaining({ 
      message: 'Hello',
    }),
    expect.anything()
  )
Enter fullscreen mode Exit fullscreen mode

expect.objectContaining() is another Jest matcher method. It compares a received object to an expected object. It will require that each property and value in the expected object is present in the received object. But, it does not work the other way.

// pass
received: { prop1: true, prop2: true }
expected: { prop1: true }

// fail
received: { prop1: true }
expected: { prop1: true, prop2: true }
Enter fullscreen mode Exit fullscreen mode

So, using expect.objectContaining() makes our test more flexible. It allows us to choose what properties we test. Here is the full updated test:

// part2/example2/__tests__/test1.js
test('The mocked ChildComponent gets called with the correct props', () => {
  render(<ParentComponent />)
  expect(ChildComponent).toHaveBeenCalledWith(
    expect.objectContaining({
      message: 'Hello',
    }),
    expect.anything()
  )
})
Enter fullscreen mode Exit fullscreen mode

We will go over the test once more. We are testing if the mock of the child component got called with the correct props, so we use the .toHaveBeenCalled() method on the mock ChildComponent. We receive two values: an object with the props and a ref value. We match the props object with expect.objectContaining because this gives us the flexibility to choose what props to test. For the second value, the ref, we use expect.anything(). We have to match the second value but we don't care about it. expect.anything() is the ideal solution.

Summary

In this article we looked at why and how to set up an (automatic) mock. We then saw how to test these mocks and how to use .toHaveBeenCalledWith().

In the next part in this series we show why and how we return values from mocks.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .