Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I test functions that are provided by context using jest and react-testing-library?

I have a <UserProvider /> component that provides context which has a login method in its value. In my test I want to check if that method has been called. How can I check to make sure this method is being called in my test if it comes from context? I've tried using the spyOn and mock methods but I can't get them to work.

Here's my UserProvider component which provides the context.

import React, { useState, useContext, createContext } from 'react'

const UserContext = createContext()

function UserProvider({ children }) {
  const [user, setUser] = useState(null)
  const login = user => setUser(user)

  return <UserContext.Provider value={{ user, login }}>{children}</UserContext.Provider>
}

const useUser = () => useContext(UserContext)

export { UserProvider, useUser }

Here's my Login component using the context from UserProvider (via useUser)

import React from 'react'
import { useUser } from './UserProvider'

function Login() {
  const { login } = useUser()

  const handleSubmit = async e => {
    e.preventDefault()

    // I need to make sure this method is being called in my test.
    login({ name: 'John Doe' })
  }

  return (
    <form onSubmit={handleSubmit}>
      <button>Login</button>
    </form>
  )
}

export default Login

Here's my Login test

import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import Login from './Login'
import { UserProvider, useUser } from './UserProvider'

// Is this correct?
jest.mock('./UserProvider', () => ({
  useUser: jest.fn(() => ({
    login: jest.fn()
  }))
}))

it('should log a user in', () => {
  const { getByText } = render(
    <UserProvider>
      <Login />
    </UserProvider>
  )

  const submitButton = getByText(/login/i)
  fireEvent.click(submitButton)

  // How can I make this work?
  const { login } = useUser()
  expect(login).toHaveBeenCalledTimes(1)
})

I have a codesandbox but it's erroring out about jest.mock not being a function so I don't know if it's very useful.

like image 256
Dennis Martinez Avatar asked Sep 12 '19 17:09

Dennis Martinez


People also ask

Does React testing library use Jest?

React Testing Library is not specific to any testing framework; we can use it with any other testing library, although Jest is recommended and preferred by many developers. create-react-app uses both Jest and React Testing Library by default.

How do you test create context?

The best way to test Context is to make our tests unaware of its existence and avoiding mocks. We want to test our components in the same way that developers would use them (behavioral testing) and mimic the way they would run in our applications (integration testing).

Which method from Jest is used to test React component?

Snapshot testing is a very useful technique to test React component snapshots using the Jest library.


Video Answer


2 Answers

I can't get it to work without slightly change the source code a little bit.

First, I have to export the actual context

export { UserProvider, useUser, UserContext }

We can re create provider with a mocked login function and the following test will serve you purpose.

import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import Login from './Login'
import { UserProvider, useUser, UserContext } from './UserProvider'

it('should log a user in', () => {
  const login = jest.fn();
  const { getByText } = render(
    <UserContext.Provider value={{ login }}>
      <Login />
    </UserContext.Provider>
  );

  const submitButton = getByText('Login');
  fireEvent.click(submitButton);

  expect(login).toHaveBeenCalledTimes(1)
});

It is entirely possible that this is not the best approach. I hope it helps.

like image 63
Andrew Zheng Avatar answered Nov 02 '22 22:11

Andrew Zheng


I see 2 more approaches here:

  1. Inject Context.Consumer alongside your component under test. Then so you would be able to verify against it
  const contextCallback = jest.fn();
  const { getByText } = render(
    <UserProvider>
      <Login />
      <UserContext.Consumer>{contextCallback}</UserContext.Consumer>
    </UserProvider>
  );

  const submitButton = getByText('Login');
  fireEvent.click(submitButton);

  expect(contextCallback.mock.calls[0][0]).toEqual({ 
    user: "John Doe"
  });
  1. Mock calls behind UserContext.Consumer(let it be speculative LoginAPI call in UserContext)
jest.mock("../LoginAPI.js");
...
  const { getByText } = render(
    <UserProvider>
      <Login />
    </UserProvider>
  );

  const submitButton = getByText('Login');
  fireEvent.click(submitButton);

  expect(LoginAPI.login).toHaveBeenCalledTimes(1);

To me 2nd one is better while 1st rely on implementation details(context data's structure).

PS to 1st approach you may declare consumer component right in the test to avoid exporting UserContext:

function ContextHelper({ spy }) {
  const contextData = useUser();
  spy(contextData);
  return null;
}

...
  const contextCallback = jest.fn();
  const { getByText } = render(
    <UserProvider>
      <Login />
      <ContextHelper spy={contextCallback} />
    </UserProvider>
  );

But keep in mind that spy may be called more times that you'd expect.

like image 25
skyboyer Avatar answered Nov 02 '22 23:11

skyboyer