Jest - How to mock a ES6 default export and then Change the default implementation/return value

Viewed 40

Background

So I am trying to test a React functional component that calls a custom hook and depending on the return value of the hook, the component will render something differently. See below for an example:


// App.js

import "./styles.css";
import useCustomHook from "./useHook";

export default function App() {
  const [state, handler] = useCustomHook();

  return state ? <div>state is true</div> : <div>state is false</div>;
}

// useHook.js
import { useState } from "react";

const useCustomHook = () => {
  const [myState, setMyState] = useState(false);

  const handleSetMyState = () => {
    return setMyState((prevState) => !prevState);
  };

  return [myState, handleSetMyState];
};

export default useCustomHook;


and finally, my test:

import "@testing-library/jest-dom";
import { render, screen, cleanup } from "@testing-library/react";
import App from "./App";

import useCustomHook from './useHook'; 

jest.mock("./useHook", () => ({
  __esModule: true,
  default: () => [false, jest.fn()]
}));

describe("testing App state", () => {
  afterEach(() => {
    jest.clearAllMocks();
    cleanup();
  });

  it("should render false when state is false", () => {
    // initialized as false AND mocked returned as false
    render(<App />);
    expect(screen.queryByText(/false/i)).toBeInTheDocument();
  });
  it("should render true when state is true", () => {
    // the following doesn't work
    useCustomHook.mockImplementation(() => [ true, jest.fn() ]) // outputs error: TypeError: _useCustomHook.default.mockImplementation is not a function

    render(<App />);
    expect(screen.queryByText(/true/i)).toBeInTheDocument();
  });
});


Now the test would pass in my local environment ( unfortunately codesandbox's environment has issues with jest.mock, so I cannot provide a live sample ) but for the next test case, if the state returned is true the component should render text with "true" in it.

Issue

However, I am unable to change the mocked implementation or the returned value of the mock for it to return something like [true, jest.fn()]

So I have been stuck on this issue for some time now and cannot find any relatable resource online that fits as a solution. I have tried the following and none worked.

  • importing the custom hook in test and changing its mocked implementation ( like shown in example )
  • same as above but changing its mockReturnValueOnce to [ true, jest.fn() ]
  • adding a __mocks__ directory in the same lvl as the hook and having a file with the same name __mocks__/useHook.js, and mock the whole module by jest.mock('./useHook') instead. Similar to this solution.
  • (updated, also tried this as latest attempt to mock) jest.mock('./useHook', () => ({ __esModule: true, default: {useCustomHook: jest.fn()}})) and it still outputs the same error when I tried to change the implementation TypeError: _useCustomHook.default.mockImplementation is not a function

Please help, something as simple as changing a mocked returned value is done so easily with other languages and testing frameworks. Why is jest complaining about?

2 Answers

The default export is not a mock function, you should use jest.fn() to create a mock function with a mock implementation.

jest.mock('./useHook', () => ({
  __esModule: true,

  // It should be
  default: jest.fn(() => [false, jest.fn()]),

  // NOT
  // default: () => [false, jest.fn()]
}));

But test implementation details are not encouraged. See What you should avoid with Testing Library

Testing Library encourages you to avoid testing implementation details like the internals of a component you're testing (though it's still possible). The Guiding Principles of this library emphasize a focus on tests that closely resemble how your web pages are interacted by the users.

You may want to avoid the following implementation details:

  1. Internal state of a component
  2. Internal methods of a component
  3. Lifecycle methods of a component
  4. Child components

I was able to find a workaround instead.


// test.js
import * as useCustomHook from './useHook';

// then inside my individual test

it('should render true when state is true"', () => {
  const mockHook = jest.spyOn(useCustomHook, 'default');
  mockHook.mockImplementation(() => [true, jest.fn()]);
  render(<App />);
  expect(screen.queryByText(/true/i)).toBeInTheDocument();
});


Not sure why jest.mock doesn't work, but I was able to make it work with jest.spyOn. If anyone is able to shed some light on why one method worked and the other didn't, it would be greatly appreciated.

Related