I’m trying to test an axios call in a debounced method, but moxios.wait() always times out if I add fake timers. The test works without the clock, if the debounce time is set small enough (e.g. 10ms) but that doesn’t help testing proper debouncing. I’ve tried experimenting with Vue.nextTick as well as making the callback to it() async, but I seem to only go further into the weeds. What’s the right approach here?
Here’s a component and test in one, that shows the problem:
import Vue from 'vue'
import { mount } from 'vue-test-utils'
import axios from 'axios'
import moxios from 'moxios'
import _ from 'lodash'
import expect from 'expect'
import sinon from 'sinon'
let Debounced = Vue.component('Debounced',
{
template: `<div><button @click.prevent="fetch"></button></div>`,
data() {
return {
data: {}
}
},
methods: {
fetch: _.debounce(async () => {
let data = await axios.post('/test', {param: 'example'})
this.data = data
}, 100)
}
}
)
describe.only ('Test axios in debounce()', () => {
let wrapper, clock
beforeEach(() => {
clock = sinon.useFakeTimers()
moxios.install()
wrapper = mount(Debounced)
})
afterEach(() => {
moxios.uninstall()
clock.restore()
})
it ('should send off a request when clicked', (done) => {
// Given we set up axios to return something
moxios.stubRequest('/test', {
status: 200,
response: []
})
// When the button is clicked
wrapper.find('button').trigger('click')
clock.tick(100)
moxios.wait(() => {
// It should have called axios with the right params
let request = moxios.requests.mostRecent()
expect(JSON.parse(request.config.data)).toEqual({param: 'example'})
done()
})
})
})
About test timeout exception: moxios.wait
relies on the setTimeout
but we replaced our setTimeout
with custom js implementation, and to make moxios.wait
work we should invoke clock.tick(1)
after wait call.
moxios.wait(() => {
// It should have called axios with the right params
let request = moxios.requests.mostRecent()
expect(JSON.parse(request.config.data)).toEqual({param: 'example'})
done()
});
clock.tick(1);
This will allow test to enter the callback body...But it will fail again with exception that request
is undefined.
Main problem: The problem is that fake timers are calling all callbacks synchronously(without using macrotask event loop) but Promises still uses event loop.
So your code just ends before any Promise "then" will be executed. Of course you can push your test code as microtasks to access request and response data, for example (using await
or wrap it as then" callback):
wrapper.find('button').trigger('click');
// debounced function called
clock.tick(100);
// next lines will be executed after an axios "then" callback where a request will be created.
await Promise.resolve();
// axios promise created, a request is added to moxios requests
// next "then" is added to microtask event loop
console.log("First assert");
let request = moxios.requests.mostRecent()
expect(JSON.parse(request.config.data)).toEqual({ param: 'example' });
// next lines will be executed after promise "then" from fetch method
await Promise.resolve();
console.log("Second assert");
expect(wrapper.vm.data.data).toEqual([]);
done();
I added another assertion for data to show that your code should "wait" for 2 "then" callbacks: axios internal "then" with your request creation and your "then" from fetch
method.
As you see I removed wait
call because it actually do nothing if you want to wait for Promises.
Yes, this is a dirty hack and closely related to axios implementation itself, but I don't have better advice and firstly I just tried to explain the issue.