pcvnes
pcvnes

Reputation: 967

Implemented function in provided context called instead of Jest mock

I need to test the click on a button in a React application which invokes a function provided by the context. When invoking the mocked function provided by the context in a Jest test the actual function is called. What to do to resolve this ?

Application App.tsx

import React, {useContext, useState} from 'react'
import styles from './App.module.scss'
import {ThemeContext} from './context/theme/ThemeProvider'

export const App = (): JSX.Element => {
  const {changeTheme, activeTheme} = useContext(ThemeContext)
  const [theme, setTheme] = useState<string>(activeTheme)

  const setNewTheme = (newTheme: string) => {
    changeTheme(newTheme)
    setTheme(newTheme)
  }

  return (
    <div className={styles.container}>
      <h1>Project Name</h1>
      <button
        data-testid='app-theme-btn1'
        onClick={() => {
          setNewTheme('dark')
        }}
      >
        Click here
      </button>
      <p>{`Active Theme: ${theme}`}</p>
    </div>
  )
}

Actual test

  test('click button changes theme', async () => {
    const defaultTheme = 'light'
    type ThemeContext = {
      activeTheme: string
      changeTheme: (theme: string) => void
    }

    const defaultThemeContext: ThemeContext = {
      activeTheme: defaultTheme,
      changeTheme: (theme) => {
        /* left empty */
      }
    }

    const ThemeContext = createContext<ThemeContext>(defaultThemeContext)

    const themeContext = {
      activeTheme: defaultTheme,
      changeTheme: jest.fn().mockImplementation((newTheme) => {
        themeContext.activeTheme = newTheme
      })
    }

    render(
      <ThemeContext.Provider value={themeContext}>
        <App />
      </ThemeContext.Provider>
    )
    screen.debug(undefined, Infinity)
    const themeButton = screen.getByTestId('app-theme-btn1')
    await userEvent.click(themeButton)
    await waitFor(() => screen.queryByText('Active Theme: dark'))
    // Error: expect(jest.fn()).toHaveBeenCalledTimes(expected)
    //
    // Expected number of calls: 1
    // Received number of calls: 0
    expect(themeContext.changeTheme).toHaveBeenCalledTimes(1)
    screen.debug(undefined, Infinity)
  })

Upvotes: 0

Views: 4093

Answers (1)

Lin Du
Lin Du

Reputation: 102307

The problem is you create a different React context in the test case rather than using the ThemeContext created in ./context/theme/ThemeProvider module.

In other words, the context used by useContext() hook in the App component should be the ThemeContext so that App component can receive the context value and subscribe to the value changes from ThemeContext.Provider.

E.g.

app.tsx:

import React, { useContext, useState } from 'react';
import { ThemeContext } from './theme-provider';

export const App = (): JSX.Element => {
  const { changeTheme, activeTheme } = useContext(ThemeContext);
  const [theme, setTheme] = useState<string>(activeTheme);

  const setNewTheme = (newTheme: string) => {
    changeTheme(newTheme);
    setTheme(newTheme);
  };

  return (
    <div>
      <h1>Project Name</h1>
      <button
        data-testid="app-theme-btn1"
        onClick={() => {
          setNewTheme('dark');
        }}
      >
        Click here
      </button>
      <p>{`Active Theme: ${theme}`}</p>
    </div>
  );
};

theme-provider.tsx:

import React, { useState } from 'react';

const defaultContext = {
  activeTheme: 'light',
  changeTheme: (newTheme: string) => {},
};
export const ThemeContext = React.createContext(defaultContext);

export const ThemeProvider = ({ children }) => {
  const [activeTheme, setActiveTheme] = useState(defaultContext.activeTheme);
  const changeTheme = (newTheme: string) => {
    setActiveTheme(newTheme);
  };
  return <ThemeContext.Provider value={{ activeTheme, changeTheme }}>{children}</ThemeContext.Provider>;
};

app.test.tsx:

import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { App } from './app';
import { ThemeContext } from './theme-provider';

describe('71901699', () => {
  test('should pass', async () => {
    const defaultTheme = 'light';
    const themeContext = {
      activeTheme: defaultTheme,
      changeTheme: jest.fn().mockImplementation((newTheme) => {
        themeContext.activeTheme = newTheme;
      }),
    };

    render(
      <ThemeContext.Provider value={themeContext}>
        <App />
      </ThemeContext.Provider>
    );
    const themeButton = screen.getByTestId('app-theme-btn1');
    userEvent.click(themeButton);
    expect(themeContext.changeTheme).toHaveBeenCalledTimes(1);
    await waitFor(() => screen.queryByText('Active Theme: dark'));
  });
});

Test result:

 PASS  stackoverflow/71901699/app.test.tsx (8.691 s)
  71901699
    ✓ should pass (74 ms)

--------------------|---------|----------|---------|---------|-------------------
File                | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
--------------------|---------|----------|---------|---------|-------------------
All files           |      80 |      100 |      50 |   77.78 |                   
 app.tsx            |     100 |      100 |     100 |     100 |                   
 theme-provider.tsx |   55.56 |      100 |       0 |      50 | 10-14             
--------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        9.217 s

Upvotes: 4

Related Questions