Search code examples
vue.jsaxiossinonmoxiosvue-test-utils

Testing debounced asynchronous request with moxios and fakeTimers


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()
        })
    })
})

Solution

  • About test timeout exception: moxios.wait relies on the setTimeout but we replaced our setTimeout with custom js implementation, and to make moxios.waitwork 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.