1

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?

0x5929
  • 400
  • 1
  • 4
  • 16

2 Answers2

2

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
Lin Du
  • 88,126
  • 95
  • 281
  • 483
  • Im sorry, it seemed like the logic solution. But when I tried it, I received: `TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator))` – 0x5929 Sep 09 '22 at 18:00
  • and you are absolutely right about the implementation details, but this seemed like such a trivial thing that I've been stuck on, and I just want to find a viable solution if I were ever to be in that issue again – 0x5929 Sep 09 '22 at 18:03
  • So I kept trying and the only way I was able to get your solution to initially work was using `..., default: ()=>jest.fn(()=>[false, jest.fn()])()`, then after changing the export's implementation it resulted in the same error. I will keep trying diff things to see and update the post, thank you – 0x5929 Sep 10 '22 at 18:37
0

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.

0x5929
  • 400
  • 1
  • 4
  • 16