Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

jest.fn() claims not to have been called, but has

I'm testing a Vue component that calls a certain action in my Vuex store when a certain parameter is present in the route. I'm mocking the action with jest.fn().

Here's the relevant code from the component:

await this.$store.dispatch('someOtherAction');
if (this.$route.params && this.$route.params.id) {
    this.$store.dispatch('selection/selectElement', parseInt(this.$route.params.id, 10));
}

Here's the mocked function:

someOtherAction = jest.fn();
selectElement = jest.fn(() => console.log("selectElement has been called"));

My test:

it('selects element if passed in route', async () => {
  const $route = {params: {id: '256'}};
  const wrapper = shallowMount(AbcModel, {
    mocks: {$route},
    store, localVue
  });
  expect(someOtherAction).toHaveBeenCalled();
  expect(selectElement).toHaveBeenCalled();
});

In the output, I can see the 'selectElement has been called'. Clearly it has been called. And yet, expect(selectElement).toHaveBeenCalled() fails.

How is this possible? It works fine with another function I mocked. Replacing the order in which I mock the functions doesn't matter. Removing the expectation that the other function gets called doesn't matter either, so it doesn't look like a collision.

like image 883
mcv Avatar asked Feb 26 '19 17:02

mcv


People also ask

What does Jest fn () do?

The Jest library provides the jest. fn() function for creating a “mock” function. An optional implementation function may be passed to jest. fn() to define the mock function's behavior and return value.

What is Jest FN mockImplementation?

mockImplementation(fn) ​ Accepts a function that should be used as the implementation of the mock. The mock itself will still record all calls that go into and instances that come from itself – the only difference is that the implementation will also be executed when the mock is called. tip.

How do you spy a function in Jest?

To spy on an exported function in jest, you need to import all named exports and provide that object to the jest. spyOn function. That would look like this: import * as moduleApi from '@module/api'; // Somewhere in your test case or test suite jest.

What is the difference between Jest FN and Jest mock?

mock replaces one module with either just jest. fn , when you call it with only the path parameter, or with the returning value of the function you can give it as the second parameter.


1 Answers

How is this possible?

The expect runs and fails before selectElement has had a chance to run.


Details

Message Queue

JavaScript uses a message queue. The current message runs to completion before the next one starts.

PromiseJobs Queue

ES6 introduced the PromiseJobs queue which handles jobs "that are responses to the settlement of a Promise". Any jobs in the PromiseJobs queue run after the current message completes and before the next message begins.

async / await

async and await are just syntactic sugar over promises and generators. Calling await on a Promise essentially wraps the rest of the function in a callback to be scheduled in PromiseJobs when the Promise resolves.

What happens

Your test starts running as the current running message. Calling shallowMount loads your component which runs until await this.$store.dispatch('someOtherAction'); which calls someOtherFunction and then essentially queues the rest of the function as a Promise callback to be scheduled in the PromiseJobs queue when the Promise resolves.

Execution then returns to the test which runs the two expect statements. The first one passes since someOtherFunction has been called, but the second fails since selectElement has not run yet.

The current running message then completes and the pending jobs in the PromiseJobs queue are then run. The callback that calls selectElement is in the queue so it runs and calls selectElement which logs to the console.


Solution

Make sure the Promise callback that calls selectElement has run before running the expect.

Whenever possible it is ideal to return the Promise so the test can await it directly.

If that is not possible then a workaround is to call await on a resolved Promise during the test which essentially queues the rest of the test at the back of the PromiseJobs queue and allows any pending Promise callbacks to run first:

it('selects element if passed in route', async () => {
  const $route = {params: {id: '256'}};
  const wrapper = shallowMount(AbcModel, {
    mocks: {$route},
    store, localVue
  });
  expect(someOtherFunction).toHaveBeenCalled();
  // Ideally await the Promise directly...
  // but if that isn't possible then calling await Promise.resolve()
  // queues the rest of the test at the back of PromiseJobs
  // allowing any pending callbacks to run first
  await Promise.resolve();
  expect(selectElement).toHaveBeenCalled();  // SUCCESS
});
like image 109
Brian Adams Avatar answered Oct 21 '22 03:10

Brian Adams