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.
- Why you need mocking to test React
- How to mock React components
- 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>
)
}
// part2/example1/ParentComponent.js
function ParentComponent(){
return(
<div className="ParentComponent">
<div>Parent Component</div>
<ChildComponent />
</div>
)
}
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()
})
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()
})
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()
})
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()
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()
})
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()
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()
})
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
// 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
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" })}
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')
}
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')
})
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' }
)
})
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:
- This ref value is usually empty (
{}
) - 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' },
{}
)
})
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()
)
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()
)
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 }
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()
)
})
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.
Top comments (10)
Thank you!! very much. It is really well written and easy to understand.
This type of blog really helps to get the overview before moving to the more advance example given in docs or other blogs.
Thanks, saved a couple of hours.
One corrections, a comma is mission in the following code snippet, end of the line
{ message: 'Hello' }
,Fixed one,
Glad you liked it, fixed the typo.
I'm getting an issue when try to mock child component
Parent.test
import ChildComponent from '../Child'
import Parent from './index'
jest.mock('../Child')
describe('Parent component', () => {
test('Correctly render ChildComponent', () => {
render(<Parent Component={ChildComponent} {...otherProps} />)
expect(ChildComponent).toHaveBeenCalled()
})
})
Child component
const Child = (props) => {
...some logic here
return (
<>
<CommonComponent>
</>
}
The error
Any idea about this?
Did you actually mount the Parent component? You have to render the parent:
render(<Parent />)
Yes I did,
render(<Parent Component={ChildComponent} {...otherProps} />)
I just fixed with this
jest.mock('../Child', () => {
return {
__esModule: true,
default: () => {
return <div>Child component</div>
}
}
})
FYI that second argument that is usually an empty object appears to be context
Well explanied
Thank you