Setting up tests with mocks and more mocking examples (Jest mocking + React part 4)

Peter Jacxsens - Sep 8 '22 - - Dev Community

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.

  1. Setting up tests with mocks
  2. 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
Enter fullscreen mode Exit fullscreen mode
// 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
Enter fullscreen mode Exit fullscreen mode

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)
})
Enter fullscreen mode Exit fullscreen mode

The second test fails:

● ChildComponent mocks gets called correctly

    expect(jest.fn()).toHaveBeenCalledTimes(expected)

    Expected number of calls: 1
    Received number of calls: 2
Enter fullscreen mode Exit fullscreen mode

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)
})
Enter fullscreen mode Exit fullscreen mode

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)
})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

So here is our child: UsersComponent. It makes a call to jsonplaceholder/users, hence UsersComponent. 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
}
Enter fullscreen mode Exit fullscreen mode

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(() => {})
Enter fullscreen mode Exit fullscreen mode

Now, let's add some values to this function. FetchComponent returns props.children(somedata).

FetchComponent.mockImplementation(
  (props) => props.children()
)
Enter fullscreen mode Exit fullscreen mode

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,
  })
)
Enter fullscreen mode Exit fullscreen mode

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()
})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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,
})
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
)
Enter fullscreen mode Exit fullscreen mode

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,
    })
  )
})
Enter fullscreen mode Exit fullscreen mode

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!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player