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

3

14

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.

Erleneerlewine answered 12/9, 2019 at 17:18 Comment(0)
O
15

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.

Obscuration answered 12/9, 2019 at 18:19 Comment(0)
P
4

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.

Peppi answered 12/9, 2019 at 19:17 Comment(2)
I'm a bit confused on approach 2. Are you recommending I test on a method that is being called from a method inside context? e.g. I call login from the context but test that loginAPI is called (which would be called from login)?Erleneerlewine
Yes, right. It's not "testing in isolation" rather more "testing integration between component and context", but in 2nd case we would be even more sure that while test passes everything works as we expect.Peppi
F
-1

contactUs.jsx

<div className="grey-box">
      <h3>{context.t("contact_us")}</h3>
      <p>{context.t("contact_us_description")}</p>

how to write test case for it in react it contain i18n-redux by using react-context

Fusilier answered 7/7, 2023 at 9:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.