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.
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.
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.
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.
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.
How is this possible?
The expect
runs and fails before selectElement
has had a chance to run.
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
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With