Тестирование асинхронной логики

Чтобы упростить тестирование, vue-test-utils применяет обновления DOM синхронно. Однако, есть некоторые тонкости, когда вам необходимо протестировать компонент с асинхронным поведением, таким как коллбэки или промисы.

Одними из самых распространённых поведений являются запросы к API и действия Vuex. В примерах ниже будет показано как протестировать метод, который делает запрос к API. В этом примере используется Jest для запуска тестов и мок для HTTP-библиотеки axios. Подробнее о использовании моков в Jest можно прочитать здесь.

Реализация мока для axios выглядит так:

export default {
  get: () => Promise.resolve({ data: 'value' })
}

Компонент ниже делает вызов к API при нажатии кнопки и сохраняет полученный ответ в 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>

Тест можно написать следующим образом:

import { shallowMount } from '@vue/test-utils'
import Foo from './Foo'
jest.mock('axios')

it('делает асинхронный запрос при нажатии кнопки', () => {
  const wrapper = shallowMount(Foo)
  wrapper.find('button').trigger('click')
  expect(wrapper.vm.value).toBe('value')
})

В настоящее время этот тест не будет успешно проходить, потому что проверка значения вызывается до разрешения промиса fetchResults. Большинство библиотек для модульного тестирования предоставляют коллбэк, чтобы предоставить возможность определять когда тест должен будет завершаться. Jest и Mocha используют done. Мы можем использовать done в комбинации с $nextTick или setTimeout, чтобы гарантировать, что любые промисы будут разрешены перед проверками.

it('делает асинхронный запрос при нажатии кнопки', done => {
  const wrapper = shallowMount(Foo)
  wrapper.find('button').trigger('click')
  wrapper.vm.$nextTick(() => {
    expect(wrapper.vm.value).toBe('value')
    done()
  })
})

Необходимость $nextTick или setTimeout требуется для прохождения теста, потому что очередь микрозадач, в которой обрабатываются промисы, обрабатывается до очереди задач, где обрабатываются $nextTick и setTimeout. Это означает, что к моменту запуска $nexTick и setTimeout, будут выполнены любые коллбэки промисов. См. здесь для более подробного объяснения.

Другое решение — использовать async функцию и npm-пакет flush-promises. flush-promises сбрасывает все ожидаемые промисы. Вы можете использовать await вызов для flushPromises чтобы очистить все ожидаемые промисы и улучшить читаемость вашего теста.

Обновлённый тест будет выглядеть так:

import { shallowMount } from '@vue/test-utils'
import flushPromises from 'flush-promises'
import Foo from './Foo'
jest.mock('axios')

it('делает асинхронный запрос при нажатии кнопки', async () => {
  const wrapper = shallowMount(Foo)
  wrapper.find('button').trigger('click')
  await flushPromises()
  expect(wrapper.vm.value).toBe('value')
})

Подобная техника может применяться и для действий Vuex, которые возвращают promise по умолчанию.