Matcher error: received value must be a mock or spy function
Asked Answered
B

4

17

I'm writing tests (with Jest and React Testing Library) for a form React component. I have a method that runs on form submit:

const onSubmit = (data) => {
  // ...
  setIsPopupActive(true);
  // ...
};

and useEffect that runs after isPopupActive change, so also on submit:

useEffect(() => {
  if (isPopupActive) {
    setTimeout(() => {
      setIsPopupActive(false);
    }, 3000);
  }
}, [isPopupActive]);

In the test, I want to check, whether the popup disappears after 3 seconds. So here's my test:

it('Closes popup after 3 seconds', async () => {
    const nameInput = screen.getByPlaceholderText('Imię');
    const emailInput = screen.getByPlaceholderText('Email');
    const messageInput = screen.getByPlaceholderText('Wiadomość');
    const submitButton = screen.getByText('Wyślij');

    jest.useFakeTimers();

    fireEvent.change(nameInput, { target: { value: 'Test name' } });
    fireEvent.change(emailInput, { target: { value: '[email protected]' } });
    fireEvent.change(messageInput, { target: { value: 'Test message' } });
    fireEvent.click(submitButton);

    const popup = await waitFor(() =>
      screen.getByText(/Wiadomość została wysłana/)
    );

    await waitFor(() => {
      expect(popup).not.toBeInTheDocument(); // this passes

      expect(setTimeout).toHaveBeenCalledTimes(1);
      expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 3000);
    });
  });

However, I'm getting the error:

expect(received).toHaveBeenCalledTimes(expected)

Matcher error: received value must be a mock or spy function

Received has type:  function
Received has value: [Function setTimeout]

What am I doing wrong?

Barrier answered 17/6, 2021 at 9:25 Comment(0)
H
25

Jest 27 has breaking changes for fakeTimers. It seems Jest contributors doesn't update documentation on time. This comment on Github issues confirms it. Moreover, here related PR.

Well, you can solve your problem by two ways.

  1. Configure Jest to use legacy fake timers. In jest.config.js you can add line (but it not works for me):
module.exports = {
 // many of lines omited
 timers: 'legacy'
};
  1. Configure legacy fake timers for individually test suite, or even test:
jest.useFakeTimers('legacy');
describe('My awesome logic', () => {
// blah blah blah
});

It's preferably to use new syntax based on @sinonjs/fake-timers. But I can't find working example for Jest, so I'll update this answer as soon as possible.

Hypochondrium answered 4/8, 2021 at 19:14 Comment(2)
option 2 listed here worked for me. I wish developers didn't release new versions without the corresponding docs. What's the rush if the docs aren't ready yet??Megagamete
It looks like these docs might be relevant jestjs.io/docs/timer-mocksResult
B
3

The below approach worked

beforeEach(() => {
  jest.spyOn(global, 'setTimeout');
});

afterEach(() => {
  global.setTimeout.mockRestore();
});

it('Test if SetTimeout is been called', {
  global.setTimeout.mockImplementation((callback) => callback());
  expect(global.setTimeout).toBeCalledWith(expect.any(Function), 7500);
})
Boulogne answered 5/8, 2021 at 12:50 Comment(0)
S
0

In your case setTimeout is not a mock or spy, rather, it's a real function. To make it a spy, use const timeoutSpy = jest.spyOn(window, 'setTimeout'). And use timeoutSpy in the assertion.

You could also test not the fact of calling the setTimeout function, but assert that setIsPopupActive was called once, and with false. For this you might need to do jest.runOnlyPendingTimers() or jest.runAllTimers()

Sculpin answered 17/6, 2021 at 21:7 Comment(3)
Unfortunately, when I replaced setTimeout with timeoutSpy, it says that the function received hasn't been called at all. jest.runOnlyPendingTimers() and jest.runAllTimers didn't help either. Are there any other options?Barrier
Did you try passing 'modern' argument as a string into jest.useFakeTimers()?Sculpin
I have the same problem and the expect(setTimeout).toHaveBeenCalledTimes(1); is explicitly mentioned in jestjs.io/fr/docs/timer-mocks ...Hundred
G
0

I also faced similar issue after I upgraded jest and fixed it. You can make spy for setTimeout or other timer related functions and then test whether it is called or not. For exmaple :

let setTimeoutSpy = jest.spyOn(window,'setTimeout');
// call you function which you want to test
jest.runAllTimers();
expect(setTimeoutSpy).toHaveBeenCalledTimes(1);
Guillema answered 12/4, 2023 at 7:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.