In this last part of the series I want to tie off some loose ends. Things I wanted to talk about earlier but didn't really fit into the structure of this series.
We will start by looking at some caveats when running tests and then look at one more example of using mocks when testing React
components.
- Setting up tests with mocks
- Mocking render props
The examples I use in this article are available on github (src/part4). 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. Setting up tests with mocks
Using mocks in tests can lead to problems. When running a test on a function or component you will often find yourself running multiple test cases. But this will influence your mocks. Remember that mocks record their own behaviour. So, when a mock got called multiple times in different tests their logs will include previous calls and this is undesired. An example:
// part4/example1/ChildComponent.js
function ChildComponent(props){
return(
<div className="ChildComponent">
Child component says {props.message}
</div>
)
}
export default ChildComponent
// part4/example1/ParentComponent.js
import ChildComponent from "./ChildComponent"
function ParentComponent(props){
return(
<div className="ParentComponent">
<div>Parent Component</div>
<ChildComponent message={props.message} />
</div>
)
}
export default ParentComponent
The parent takes a message prop and then the child prints the message. When testing the parent, we mock the child and we run multiple tests:
// part4/example1/__tests__/test1.js
import { screen, render } from '@testing-library/react'
import ParentComponent from '../ParentComponent'
import ChildComponent from '../ChildComponent'
jest.mock('../ChildComponent')
test('Parent renders correctly', () => {
render(<ParentComponent />)
expect(screen.getByText(/Parent component/i)).toBeInTheDocument()
})
test('ChildComponent mocks gets called correctly', () => {
render(<ParentComponent />)
expect(ChildComponent).toHaveBeenCalled()
// fails
expect(ChildComponent).toHaveBeenCalledTimes(1)
})
The second test fails:
● ChildComponent mocks gets called correctly
expect(jest.fn()).toHaveBeenCalledTimes(expected)
Expected number of calls: 1
Received number of calls: 2
Because the mock of the child was actually called 2 times. We rendered once in the first test and again in the second. Hence, 2. But, as said, this is undesirable. We don't want to let results from previous tests pollute other tests. So, how do we solve this?
An obvious way could be to call jest.mock
inside our test. But this is not allowed. Jest
mocks modules by hoisting the mock above the import (I don't fully understand this myself). But, Jest
can't hoist when the jest.mock
statement is inside the test.
That is why Jest
provided some more helpers: the most important are .clearAllMocks()
and .resetAllMocks()
.
1.1 .clearAllMocks()
When calling jest.clearAllMocks()
the 'logs' of all the mocks are cleared of the data the mocks got called with. This means that ChildComponent.mock.calls
got reset to []
. Jest
helper functions like .toHaveBeenCalledTimes()
also got reset. A new test to demonstrate this:
// part4/example1/__tests__/test2.js
import { screen, render } from '@testing-library/react'
import ParentComponent from '../ParentComponent'
import ChildComponent from '../ChildComponent'
jest.mock('../ChildComponent')
test('Parent renders correctly', () => {
render(<ParentComponent message="Hello" />)
expect(screen.getByText(/Parent component/i)).toBeInTheDocument()
})
test('ChildComponent mocks gets called correctly', () => {
// clear
jest.clearAllMocks()
render(<ParentComponent message="Hello" />)
expect(ChildComponent).toHaveBeenCalledTimes(1)
expect(ChildComponent).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Hello'
}),
expect.anything()
)
})
test('ChildComponent mock got reset', () => {
// clear
jest.clearAllMocks()
expect(ChildComponent).toHaveBeenCalledTimes(0)
expect(ChildComponent.mock.results).toHaveLength(0)
})
Besides the .clearAllMocks()
method there is also a mockClear
method that can be called on an individual mock.
1.2 .resetAllMocks()
This method does the same as the clear method: it resets the calls (and the instances). But on top of that it also resets the results or the return value of the mock.
// part4/example1/__tests__/test3.js
import { render } from '@testing-library/react'
import ParentComponent from '../ParentComponent'
import ChildComponent from '../ChildComponent'
jest.mock('../ChildComponent')
test('Child returns foo', () => {
ChildComponent.mockReturnValue('Foo')
render(<ParentComponent message="Hello" />)
expect(ChildComponent).toHaveReturnedWith('Foo')
})
test('Child returns nothing', () => {
// reset
jest.resetAllMocks()
expect(ChildComponent.mock.results).toHaveLength(0)
})
Besides the .resetAllMocks()
method there is also a mockReset
method that can be called on an individual mock.
1.3 beforeEach()
As you probably already guessed, the beforeEach()
global jest method is ideal for running clearing or resetting mocks.
1.4 tool presets
Because clearing and resetting mocks using .beforeEach()
or .afterEach()
is such a common pratice, a lot of tools like create-react-app
have a build-in settings for this. create-react-app
has a default setting of resetting beforeEach. This caused me a lot of confusion while writing this article because I'm used to create-next-app
that has a different default setting.
I ended up changing the setup of the example files of this series (build with create-react-app
) by adding this rule "jest": { "resetMocks": false }
to the package.json file.
2. mocking render props
As a the final section in this series, we will go over the render props patterns and how to test it with mocks.
2.1 About render props
Let's take this example:
// part4/example2/FetchComponent.js
import useFetch from 'react-fetch-hook'
function FetchComponent(props){
const { isLoading, data, error } = useFetch(props.url)
return props.children({ isLoading, error, data })
}
export default FetchComponent
We have a FetchComponent
that takes a url and a child as props. It fetches the url and returns the results to a child component by calling props.children as a function, passing in the results of this fetch: props.children({ isLoading, error, data })
.
FetchComponent
takes children as a prop and then returns children with some extra data.
// component receives
prop.children
// component returns
props.children(data)
This is called render props pattern and it is used to pass data to any child. The catch when using this technique is that the child of this FetchComponent
has to be a function, not a component. Here is an example of that:
// part4/example2/UsersComponent.js
import FetchComponent from "./FetchComponent";
function UsersComponent(){
const url = `https://jsonplaceholder.typicode.com/users/`
return(
<FetchComponent url={url}>
{({ isLoading, error, data }) => {
if( isLoading ) return '...loading'
if( error ) return 'Error'
return data.map(user => <div key={user.id}>{user.name}</div>)
}}
</FetchComponent>
)
}
export default UsersComponent
So here is our child: UsersComponent
. It makes a call to jsonplaceholder/users, hence Users
Component
. Our UsersComponent
wraps it's content inside FetchComponent
that, as we know returns a function. In our users component you can see that we provided a function as child:
({ isLoading, error, data }) => {
// function body
}
Inside the function body, UsersComponent
now has access to the data from the fetch component.
2.2 Testing UsersComponent
We start with testing the UsersComponent
. What do we need to do? Mock the FetchComponent
because we want to test UsersComponent
in isolation. But, our UsersComponent
relies on the return value of the FetchComponent
. So, we have to add a return value on the mock. What does the fetch component return? A function.That's easy:
// mock the component
jest.mock('../FetchComponent')
// return a function
FetchComponent.mockImplementation(() => {})
Now, let's add some values to this function. FetchComponent
returns props.children(somedata)
.
FetchComponent.mockImplementation(
(props) => props.children()
)
And then as a final step, we add some data that we just manually write. What data? The results from the fetch hook:
FetchComponent.mockImplementation(
(props) => props.children({
isLoading: true,
error: false,
data: undefined,
})
)
And that's all. Let's go over this again. We are mocking the return value from FetchComponent
. We know that FetchComponent
returns props.children(data)
so we returned that from our mock using some made up data. The mock of FetchComponent
now returns what is required and we can run the test on our UsersComponent
.
Below is the full test file. We simulate different scenarios: a loading state, an error state, ... and run tests on each scenario.
// part4/example2/__tests__/UsersComponent.test.js
import { render, screen } from '@testing-library/react'
import UsersComponent from '../UsersComponent'
import FetchComponent from '../FetchComponent'
jest.mock('../FetchComponent')
beforeEach(() => {
jest.resetAllMocks()
})
test('UsersComponent renders correctly with loading state', () => {
FetchComponent.mockImplementation(
// eslint-disable-next-line testing-library/no-node-access
(props) => props.children({
isLoading: true,
error: false,
data: undefined,
})
)
render(<UsersComponent />)
expect(FetchComponent).toHaveBeenCalled()
expect(screen.getByText(/...loading/)).toBeInTheDocument()
})
test('UsersComponent renders correctly with error state', () => {
FetchComponent.mockImplementation(
// eslint-disable-next-line testing-library/no-node-access
(props) => props.children({
isLoading: false,
error: true,
data: undefined,
}))
render(<UsersComponent />)
expect(FetchComponent).toHaveBeenCalled()
expect(screen.getByText(/Error/)).toBeInTheDocument()
})
test('UsersComponent renders correctly with no isLoading, no error and data', () => {
FetchComponent.mockImplementation(
// eslint-disable-next-line testing-library/no-node-access
(props) => props.children({
isLoading: false,
error: undefined,
data: [
{ name: 'Foo', id: '1' },
{ name: 'Bar', id: '2' },
],
}))
render(<UsersComponent />)
expect(FetchComponent).toHaveBeenCalled()
expect(screen.getByText(/Foo/)).toBeInTheDocument()
expect(screen.getByText(/Bar/)).toBeInTheDocument()
})
The essential take away of this test if that we return a function from our mock because the component also returns a function.
2.3 Testing FetchComponent
Let's now test the FetchComponent
. Here is that component again:
// part4/example2/FetchComponent.js
import useFetch from 'react-fetch-hook'
function FetchComponent(props){
const {isLoading, data, error} = useFetch(props.url)
return props.children({ isLoading, error, data })
}
export default FetchComponent
Obviously, we want to mock useFetch
. We also want to return results from this mock: isLoading, error and data. Let's do that:
// mock
jest.mock('react-fetch-hook')
// return results
useFetch.mockReturnValue({
isLoading: true,
error: undefined,
data: undefined,
})
Notice how we are simply returning an object with 3 properties using .mockReturnValue()
. We are not returning a function because that is not what useFetch()
does.
What else do we need? FetchComponent
returns something. So, when we test FetchComponent
, we want to test if it actually returns something.
Remember that FetchComponent
receives a child then returns that child with some extra data:
// component receives
prop.children
// component returns
props.children(data)
But, when we test FetchComponent
in isolation, there is no child. So, we will mock a child. What does this child look like?
<FetchComponent url={url}>
{({ isLoading, error, data }) => {
// function body
}}
</FetchComponent>
It is a function, that takes the data and then returns something. Does the child mock need a return value? No. We are interested in what the child gets called with because that is part of the functionality of FetchComponent
. The return value of child is of no use. Let's now mock the child using a mock implementation:
// the mock
const ChildMock = jest.fn((props) => null)
// the render
render(
<FetchComponent url="dummy">
{ ChildMock }
</FetchComponent>
)
And that is all we need. ChildMock
takes the data passed by fetch and simply returns nothing. Here is the full test:
// part4/example2/__tests__/FetchComponent.test.js
import { render } from '@testing-library/react'
import FetchComponent from '../FetchComponent'
import useFetch from 'react-fetch-hook'
jest.mock('react-fetch-hook')
const ChildMock = jest.fn(props => null)
test('FetchComponent mocks useFetch correctly', () => {
useFetch.mockReturnValue({
isLoading: true,
error: undefined,
data: undefined,
})
render(
<FetchComponent url="dummy">
{ChildMock}
</FetchComponent>
)
// check the useFetch mock
expect(useFetch).toHaveBeenCalledWith('dummy')
expect(useFetch).toHaveReturnedWith(
expect.objectContaining({
isLoading: true,
error: undefined,
data: undefined,
})
)
})
test('FetchComponent ChildMock works correctly', () => {
expect(ChildMock).toHaveBeenCalledWith(
expect.objectContaining({
isLoading: true,
error: undefined,
data: undefined,
})
)
})
I hope these tests make sense. Explaining how to test render props is quite difficult. If you couldn't quite follow I recommend you reread previous section or try writing a test for these components yourself.
Conclusion
We talked about a lot in this series. We started with a general introduction of Jest
mocks: why and how to use mocks. We then moved on to using mocks to test React
components. We saw how to mock a module and different ways to run tests on mocks. We also saw how to setup mocks so they return values and why you need to do this. We used different React
patterns to demonstrate all of this.
All of this has given you a good understanding of using Jest
mocks to test React
components. Although there is much more to learn you will now be able to integrate this into what you learned here.
I hope you enjoyed this series and happy testing!
Top comments (0)