To simplify testing, Vue Test Utils applies DOM updates synchronously. However, there are some techniques you need to be aware of when testing a component with asynchronous behavior such as callbacks or promises.
One of the most common asynchronous behaviors is API calls and Vuex actions. The following examples shows how to test a method that makes an API call. This example uses Jest to run the test and to mock the HTTP library axios
. More about Jest manual mocks can be found here.
The implementation of the axios
mock looks like this:
export default {
get: () => Promise.resolve({ data: 'value' })
}
The below component makes an API call when a button is clicked, then assigns the response to value
.
<template>
<button @click="fetchResults" />
</template>
<script>
import axios from 'axios'
export default {
data() {
return {
value: null
}
},
methods: {
async fetchResults() {
const response = await axios.get('mock/service')
this.value = response.data
}
}
}
</script>
A test can be written like this:
import { shallowMount } from '@vue/test-utils'
import Foo from './Foo'
jest.mock('axios')
it('fetches async when a button is clicked', () => {
const wrapper = shallowMount(Foo)
wrapper.find('button').trigger('click')
expect(wrapper.vm.value).toBe('value')
})
This test currently fails because the assertion is called before the promise in fetchResults
resolves. Most unit test libraries provide a callback to let the runner know when the test is complete. Jest and Mocha both use done
. We can use done
in combination with $nextTick
or setTimeout
to ensure any promises resolve before the assertion is made.
it('fetches async when a button is clicked', done => {
const wrapper = shallowMount(Foo)
wrapper.find('button').trigger('click')
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.value).toBe('value')
done()
})
})
The reason $nextTick
or setTimeout
allow the test to pass is because the microtask queue where promise callbacks are processed run before the task queue, where $nextTick
and setTimeout
are processed. This means by the time the $nextTick
and setTimeout
run, any promise callbacks on the microtask queue will have been executed. See here for a more detailed explanation.
Another solution is to use an async
function and the npm package flush-promises
. flush-promises
flushes all pending resolved promise handlers. You can await
the call of flushPromises
to flush pending promises and improve the readability of your test.
The updated test looks like this:
import { shallowMount } from '@vue/test-utils'
import flushPromises from 'flush-promises'
import Foo from './Foo'
jest.mock('axios')
it('fetches async when a button is clicked', async () => {
const wrapper = shallowMount(Foo)
wrapper.find('button').trigger('click')
await flushPromises()
expect(wrapper.vm.value).toBe('value')
})
This same technique can be applied to Vuex actions, which return a promise by default.