You’re browsing the documentation for Vue Test Utils for Vue v2.x and earlier.

To read docs for Vue Test Utils for Vue 3, click here.

Руководства

Введение

Настройка

Для быстрого старта работы с Vue Test Utils, клонируйте наш демонстрационный репозиторий с базовыми настройками и установите зависимости:

git clone https://github.com/vuejs/vue-test-utils-getting-started
cd vue-test-utils-getting-started
npm install

Вы увидите, что проект содержит простой компонент counter.js:

// counter.js

export default {
  template: `
    <div>
      <span class="count">{{ count }}</span>
      <button @click="increment">Increment</button>
    </div>
  `,

  data() {
    return {
      count: 0
    }
  },

  methods: {
    increment() {
      this.count++
    }
  }
}

Монтирование компонентов

Vue Test Utils тестирует компоненты Vue монтируя их изолированно, создавая моки необходимых входных данных (входные параметры, инъекции и пользовательские события) и выполняя проверки над результатом (результат рендеринга, вызванные пользовательские события).

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

Вы можете создавать wrapper с помощью метода mount. Давайте создадим файл test.js:

// test.js

// Импортируем метод `mount()` из `vue-test-utils`
// и компонент, который хотим протестировать
import { mount } from '@vue/test-utils'
import Counter from './counter'

// Теперь монтируем компонент и у нас появляется wrapper
const wrapper = mount(Counter)

// Вы можете получить доступ к экземпляру Vue через `wrapper.vm`
const vm = wrapper.vm

// Чтобы изучить wrapper подробнее, просто выведите его в консоль
// и ваши приключения с `vue-test-utils` начнутся
console.log(wrapper)

Тестирование отрендеренного HTML компонента

Теперь, когда у нас есть wrapper, первой вещью, которую мы можем захотеть проверить что отрендеренный HTML компонента соответствует нашим ожиданиям.

import { mount } from '@vue/test-utils'
import Counter from './counter'

describe('Компонент Counter', () => {
  // Теперь монтируем компонент и получаем wrapper
  const wrapper = mount(Counter)

  it('отображает корректную разметку', () => {
    expect(wrapper.html()).toContain('<span class="count">0</span>')
  })

  // также легко проверить наличие других элементов
  it('имеет кнопку', () => {
    expect(wrapper.contains('button')).toBe(true)
  })
})

Теперь запустите тесты командой npm test. Вы должны увидеть, что все тесты проходят успешно.

Симуляция пользовательских действий

Наш счётчик должен увеличивать значение, когда пользователь нажимает кнопку. Чтобы симулировать это поведение, нам необходимо сначала получить кнопку с помощью wrapper.find(), который возвращает wrapper для элемента кнопки. Мы можем симулировать клик с помощью вызова .trigger() на wrapper кнопки:

it('нажатие кнопки должно увеличивать счётчик', () => {
  expect(wrapper.vm.count).toBe(0)
  const button = wrapper.find('button')
  button.trigger('click')
  expect(wrapper.vm.count).toBe(1)
})

Что делать с nextTick?

Vue собирает пачку предстоящих обновлений DOM и применяет их асинхронно для избежания ненужных повторных рендерингов, вызываемых множественными изменениями данных. Вот почему на практике нe часто приходится использовать Vue.nextTick для ожидания, пока Vue выполнит фактическое обновление DOM после того, как мы инициируем некоторое изменение состояния.

Для упрощения работы, vue-test-utils применяет все обновления синхронно, поэтому вам не потребуется использовать Vue.nextTick для ожидания обновления DOM в ваших тестах.

Примечание: nextTick по-прежнему необходим, когда вам нужно явно форсировать цикл событий, для таких операций как асинхронные обратные вызовы или разрешение промисов.

Если вам всё ещё нужно использовать nextTick в ваших тестовых файлах, имейте ввиду, что любые ошибки, выброшенные внутри него, могут не быть отловлены вашей программой для запуска тестов, поскольку внутри он реализован на Promise. Существует два подхода исправления этого: либо вы можете установить коллбэк done как глобальный обработчик ошибок Vue в начале теста, либо вы можете вызывать nextTick без аргумента и вернуть его как Promise:

// эта ошибка не будет отловлена
it('ошибка не будет отслеживаться', done => {
  Vue.nextTick(() => {
    expect(true).toBe(false)
    done()
  })
})

// два следующих теста будут работать как ожидается
it('должен отлавливать ошибку с использованием done', done => {
  Vue.config.errorHandler = done
  Vue.nextTick(() => {
    expect(true).toBe(false)
    done()
  })
})

it('должен отлавливать ошибку с использованием promise', () => {
  return Vue.nextTick().then(function () {
    expect(true).toBe(false)
  })
})

Что дальше

Общие советы

Понимание что тестировать

Для компонентов пользовательского интерфейса мы не рекомендуем стремиться к покрытию каждой строки кода, поскольку это приводит к слишком большому фокусу на деталях внутренней реализации компонентов и может привести к созданию хрупких тестов.

Вместо этого, мы рекомендуем писать тесты, которые проверяют ваш публичный интерфейс взаимодействия с компонентом и относиться к его внутренностям как к чёрному ящику. Один тестовый пример должен проверять, что некоторые входные данные (взаимодействие пользователя или изменение входных параметров), предоставляемые компоненту будут приводить к ожидаемому результату (результату рендеринга или вызванным пользовательским событиям).

Например, для компонента Counter, который при каждом нажатии кнопки будет увеличивать отображаемый счётчик на 1, может тестироваться с помощью симуляции клика и проверке, что в отрендренном результате значение будет увеличено на 1. Тест не заботится о том, каким образом Counter увеличивает значение, он сосредоточен только на входных данных и результате.

Преимуществом этого подхода в том, что до тех пор пока интерфейс вашего компонента остаётся прежним, ваши тесты будут проходить независимо от того, как будет меняться внутренняя реализация компонента с течением времени.

Эта тема обсуждается более подробно в отличной презентации Matt O'Connell.

Поверхностный рендеринг

В модульных тестах мы обычно хотим сосредоточиться на тестируемом компоненте, как на изолированном блоке и избежать неявной проверки поведения его дочерних компонентов.

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

vue-test-utils позволяет вам монтировать компонент без рендеринга его дочерних компонентов (заменяя их заглушками) с помощью метода shallowMount:

import { shallowMount } from '@vue/test-utils'

const wrapper = shallowMount(Component)
wrapper.vm // примонтированный экземпляр Vue

Проверка вызванных событий

Каждая примонтированная обёртка автоматически записывает все события, вызванные на экземпляре Vue. Вы можете получить записанные события с помощью метода wrapper.emitted():

wrapper.vm.$emit('foo')
wrapper.vm.$emit('foo', 123)

/*
`wrapper.emitted()` возвращает указанный объект:
{
  foo: [[], [123]]
}
*/

Затем вы можете добавить проверки на основе этих данных:

// проверка, что событие было вызвано
expect(wrapper.emitted().foo).toBeTruthy()

// проверка, что событие вызывалось определённое число раз
expect(wrapper.emitted().foo.length).toBe(2)

// проверка, что с событием были переданы определённые данные
expect(wrapper.emitted().foo[1]).toEqual([123])

Вы также можете получить массив событий в порядке их вызова с помощью wrapper.emittedByOrder().

События дочерних компонентов

Вы можете создавать пользовательские события в дочерних компонентах получая доступ к экземпляру.

Тестируемый компонент

<template>
  <div>
    <child-component @custom="onCustom" />
    <p v-if="emitted">Emitted!</p>
  </div>
</template>

<script>
  import ChildComponent from './ChildComponent'

  export default {
    name: 'ParentComponent',
    components: { ChildComponent },
    data() {
      return {
        emitted: false
      }
    },
    methods: {
      onCustom() {
        this.emitted = true
      }
    }
  }
</script>

Тест

import { shallowMount } from '@vue/test-utils'
import ParentComponent from '@/components/ParentComponent'
import ChildComponent from '@/components/ChildComponent'

describe('ParentComponent', () => {
  it("displays 'Emitted!' when custom event is emitted", () => {
    const wrapper = shallowMount(ParentComponent)
    wrapper.find(ChildComponent).vm.$emit('custom')
    expect(wrapper.html()).toContain('Emitted!')
  })
})

Манипулирование состоянием компонента

Вы можете напрямую манипулировать состоянием компонента с помощью методов setData или setProps на обёртке:

it('manipulates state', async () => {
  await wrapper.setData({ count: 10 })

  await wrapper.setProps({ foo: 'bar' })
})

Моки входных параметров

Вы можете передать входные параметры в компонент с использованием встроенной во Vue опции propsData:

import { mount } from '@vue/test-utils'

mount(Component, {
  propsData: {
    aProp: 'some value'
  }
})

Вы также можете обновить входные параметры на уже примонтированном компоненте с помощью метода wrapper.setProps({}).

Полный список опции можно посмотреть в секции настроек монтирования документации.

Добавление глобальных плагинов и примесей

Некоторые из компонентов могут полагаться на функции, добавляемые глобальным плагином или примесью, к примеру vuex и vue-router.

Если вы пишете тесты для компонентов определённого приложения, вы можете настроить одни и те же глобальные плагины и примеси один раз перед началом ваших тестов. Но в некоторых случаях, например при тестировании набора общих компонентов, которые могут использоваться в разных приложениях, гораздо лучше протестировать ваши компоненты в более изолированной конфигурации, без загрязнения глобального конструктора Vue. Мы можем использовать метод createLocalVue для достижения этого:

import { createLocalVue } from '@vue/test-utils'

// создаём расширенный конструктор `Vue`
const localVue = createLocalVue()

// устанавливаем плагины как обычно
localVue.use(MyPlugin)

// передаём `localVue` в настройки монтирования
mount(Component, {
  localVue
})

Обратите внимание, что некоторые плагины, такие как Vue Router, добавляют свойства только для чтения к глобальному конструктору Vue. Это делает невозможным переустановку плагина на конструкторе localVue или добавление моков для этих свойств

Создание моков инъекций

Другая стратегия для инъекции входных параметров — просто создание их моков. Вы можете это сделать с помощью опции mocks:

import { mount } from '@vue/test-utils'

const $route = {
  path: '/',
  hash: '',
  params: { id: '123' },
  query: { q: 'hello' }
}

mount(Component, {
  mocks: {
    // добавление мока объекта `$route` в экземпляр Vue
    // перед монтированием компонента
    $route
  }
})

Подмена компонентов

Вы можете переопределить компоненты, зарегистрированные глобально или локально, используя опцию stubs:

import { mount } from '@vue/test-utils'

mount(Component, {
  // Компонент globally-registered-component
  // будет заменяться пустой заглушкой
  stubs: ['globally-registered-component']
})

Работа с маршрутизацией

Поскольку маршрутизация по определению имеет отношение к общей структуре приложения и включает в себя несколько компонентов, её лучше всего тестировать с помощью интеграционных или end-to-end тестов. Для отдельных компонентов, которые используют возможности vue-router, вы можете создать моки с использованием упомянутых выше методов.

Обнаружение стилей

Ваш тест может обнаруживать только встроенные стили при в jsdom.

Тестирование нажатий клавиш, мыши и других событий DOM

Генерация событий

Wrapper предоставляет метод trigger. Его можно использовать для генерации событий DOM.

const wrapper = mount(MyButton)

wrapper.trigger('click')

Вы должны помнить, что метод find также возвращает Wrapper. Предполагается, что MyComponent содержит кнопку, а следующий код нажимает эту кнопку.

const wrapper = mount(MyComponent)

wrapper.find('button').trigger('click')

Опции

Метод trigger также может опционально принимать объект options. Свойства объекта options добавятся к Event.

Обратите внимание, что цель (target) не может добавлена в объект options.

const wrapper = mount(MyButton)

wrapper.trigger('click', { button: 0 })

Пример тестирования кнопки мыши

Тестируемый компонент

<template>
  <div>
    <button class="yes" @click="callYes">Yes</button>
    <button class="no" @click="callNo">No</button>
  </div>
</template>

<script>
  export default {
    name: 'YesNoComponent',

    props: {
      callMe: {
        type: Function
      }
    },

    methods: {
      callYes() {
        this.callMe('yes')
      },
      callNo() {
        this.callMe('no')
      }
    }
  }
</script>

Тест

import YesNoComponent from '@/components/YesNoComponent'
import { mount } from '@vue/test-utils'
import sinon from 'sinon'

describe('Click event', () => {
  it('Нажатие на кнопку yes вызывает наш метод с аргументом "yes"', () => {
    const spy = sinon.spy()
    const wrapper = mount(YesNoComponent, {
      propsData: {
        callMe: spy
      }
    })
    wrapper.find('button.yes').trigger('click')

    spy.should.have.been.calledWith('yes')
  })
})

Пример тестирования клавиши

Тестируемый компонент

Этот компонент позволяет увеличивать/уменьшать количество с помощью различных клавиш.

<template>
  <input type="text" @keydown.prevent="onKeydown" v-model="quantity" />
</template>

<script>
  const KEY_DOWN = 40
  const KEY_UP = 38
  const ESCAPE = 27

  export default {
    data() {
      return {
        quantity: 0
      }
    },

    methods: {
      increment() {
        this.quantity += 1
      },
      decrement() {
        this.quantity -= 1
      },
      clear() {
        this.quantity = 0
      },
      onKeydown(e) {
        if (e.keyCode === ESCAPE) {
          this.clear()
        }
        if (e.keyCode === KEY_DOWN) {
          this.decrement()
        }
        if (e.keyCode === KEY_UP) {
          this.increment()
        }
        if (e.key === 'a') {
          this.quantity = 13
        }
      }
    },

    watch: {
      quantity: function (newValue) {
        this.$emit('input', newValue)
      }
    }
  }
</script>

Тест

import QuantityComponent from '@/components/QuantityComponent'
import { mount } from '@vue/test-utils'

describe('Тестирование событий клавиш', () => {
  it('Quantity по умолчанию равно нулю', () => {
    const wrapper = mount(QuantityComponent)
    expect(wrapper.vm.quantity).toBe(0)
  })

  it('Клавиша вверх увеличивает quantity на 1', () => {
    const wrapper = mount(QuantityComponent)
    wrapper.trigger('keydown.up')
    expect(wrapper.vm.quantity).toBe(1)
  })

  it('Клавиша вниз уменьшает quantity на 1', () => {
    const wrapper = mount(QuantityComponent)
    wrapper.vm.quantity = 5
    wrapper.trigger('keydown.down')
    expect(wrapper.vm.quantity).toBe(4)
  })

  it('Escape устанавливает quantity равным 0', () => {
    const wrapper = mount(QuantityComponent)
    wrapper.vm.quantity = 5
    wrapper.trigger('keydown.esc')
    expect(wrapper.vm.quantity).toBe(0)
  })

  it('Магический символ "a" устанавливает quantity равным 13', () => {
    const wrapper = mount(QuantityComponent)
    wrapper.trigger('keydown', {
      key: 'a'
    })
    expect(wrapper.vm.quantity).toBe(13)
  })
})

Ограничения

Имя модификатора после точки keydown.up преобразуется в keyCode. Это поддерживается для следующих имён:

key name key code
enter 13
esc 27
tab 9
space 32
delete 46
backspace 8
insert 45
up 38
down 40
left 37
right 39
end 35
home 36
pageup 33
pagedown 34

Важно

Vue Test Utils генерирует событие синхронно. Следовательно, Vue.nextTick не требуется.

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

Чтобы упростить тестирование, 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', () => ({
  get: Promise.resolve({ data: 'value' })
}))

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. Это означает, что к моменту запуска $nextTick и 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 по умолчанию.

Using with TypeScript

An example project for this setup is available on GitHub.

TypeScript is a popular superset of JavaScript that adds types and classes on top of regular JS. Vue Test Utils includes types in the distributed package, so it works well with TypeScript.

In this guide, we'll walk through how to setup a testing setup for a TypeScript project using Jest and Vue Test Utils from a basic Vue CLI TypeScript setup.

Adding TypeScript

First you need to create a project. If you don't have Vue CLI installed, install it globally:

$ npm install -g @vue/cli

And create a project by running:

$ vue create hello-world

In the CLI prompt, choose to Manually select features, select TypeScript, and press enter. This will create a project with TypeScript already configured.

NOTE

If you want a more detailed guide on setting up Vue with TypeScript, checkout the TypeScript Vue starter guide.

The next step is to add Jest to the project.

Setting up Jest

Jest is a test runner developed by Facebook, aiming to deliver a battery-included unit testing solution. You can learn more about Jest on its official documentation.

Install Jest and Vue Test Utils:

$ npm install --save-dev jest @vue/test-utils

Next define a test:unit script in package.json.

// package.json
{
  // ..
  "scripts": {
    // ..
    "test:unit": "jest"
  }
  // ..
}

Processing Single-File Components in Jest

To teach Jest how to process *.vue files, we need to install and configure the vue-jest preprocessor:

npm install --save-dev vue-jest

Next, create a jest block in package.json:

{
  // ...
  "jest": {
    "moduleFileExtensions": [
      "js",
      "ts",
      "json",
      // tell Jest to handle `*.vue` files
      "vue"
    ],
    "transform": {
      // process `*.vue` files with `vue-jest`
      ".*\\.(vue)$": "vue-jest"
    },
    "testURL": "http://localhost/"
  }
}

Configuring TypeScript for Jest

In order to use TypeScript files in tests, we need to set up Jest to compile TypeScript. For that we need to install ts-jest:

$ npm install --save-dev ts-jest

Next, we need to tell Jest to process TypeScript test files with ts-jest by adding an entry under jest.transform in package.json:

{
  // ...
  "jest": {
    // ...
    "transform": {
      // ...
      // process `*.ts` files with `ts-jest`
      "^.+\\.tsx?$": "ts-jest"
    }
    // ...
  }
}

Placing Test Files

By default, Jest will recursively pick up all files that have a .spec.js or .test.js extension in the entire project.

To run test files with a .ts extension, we need to change the testRegex in the config section in the package.json file.

Add the following to the jest field in package.json:

{
  // ...
  "jest": {
    // ...
    "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$"
  }
}

Jest recommends creating a __tests__ directory right next to the code being tested, but feel free to structure your tests as you see fit. Just beware that Jest would create a __snapshots__ directory next to test files that performs snapshot testing.

Writing a unit test

Now we've got the project set up, it's time to write a unit test.

Create a src/components/__tests__/HelloWorld.spec.ts file, and add the following code:

// src/components/__tests__/HelloWorld.spec.ts
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '../HelloWorld.vue'

describe('HelloWorld.vue', () => {
  test('renders props.msg when passed', () => {
    const msg = 'new message'
    const wrapper = shallowMount(HelloWorld, {
      propsData: { msg }
    })
    expect(wrapper.text()).toMatch(msg)
  })
})

That's all we need to do to get TypeScript and Vue Test Utils working together!

Resources

Использование с Vue Router

Установка Vue Router в тестах

Вы никогда не должны устанавливать Vue Router в базовый конструктор Vue в тестах. Установка Vue Router добавляет $route и $router как свойства только для чтения на прототипе Vue.

Чтобы этого избежать, мы можем создать localVue и установить Vue Router на него.

import { shallowMount, createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'

const localVue = createLocalVue()
localVue.use(VueRouter)
const router = new VueRouter()

shallowMount(Component, {
  localVue,
  router
})

Примечание: Установка Vue Router на localVue также добавляет $route и $router в качестве свойство только для чтения localVue. Это означает, что вы можете использовать опцию mocks для перезаписи $route и $router при монтировании компонента, используя localVue с установленным Vue Router.

Когда вы устанавливаете Vue Router, регистрируются глобальные компоненты router-link и router-view. Это означает, что мы можем использовать их в любом месте нашего приложения без необходимости импортировать их.

Когда мы запускаем тесты, нам нужно сделать эти компоненты vue-router доступными для компонента, который мы монтируем. Есть два способа сделать это.

Использование заглушек (stubs)

import { shallowMount } from '@vue/test-utils'

shallowMount(Component, {
  stubs: ['router-link', 'router-view']
})

Установка Vue Router с помощью localVue

import { shallowMount, createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'

const localVue = createLocalVue()
localVue.use(VueRouter)

shallowMount(Component, {
  localVue
})

Создание моков $route и $router

Иногда вам может потребоваться протестировать, что компонент что-то делает с параметрами объектов $route и $router. Для этого вы можете передавать пользовательские моки в экземпляр Vue.

import { shallowMount } from '@vue/test-utils'

const $route = {
  path: '/some/path'
}

const wrapper = shallowMount(Component, {
  mocks: {
    $route
  }
})

wrapper.vm.$route.path // /some/path

Известные подводные камни

Установка Vue Router добавляет $route и $router в качестве свойств только для чтения на прототипе Vue.

Это означает, что любые будущие тесты, которые попытаются сделать мок $route или $router потерпят неудачу.

Для избежания этого никогда не устанавливайте Vue Router глобально при запуске тестов; используйте localVue как было показано выше.

Использование с Vuex

В этом руководстве мы рассмотрим как тестировать Vuex в компонентах с Vue Test Utils и как подходить к тестированию хранилища Vuex.

Тестирование Vuex в компонентах

Создание моков для действий

Давайте посмотрим на часть кода.

Это компонент который мы хотим протестировать. Он вызывает действие Vuex.

<template>
  <div class="text-align-center">
    <input type="text" @input="actionInputIfTrue" />
    <button @click="actionClick()">Нажми</button>
  </div>
</template>

<script>
  import { mapActions } from 'vuex'

  export default {
    methods: {
      ...mapActions(['actionClick']),
      actionInputIfTrue: function actionInputIfTrue(event) {
        const inputValue = event.target.value
        if (inputValue === 'input') {
          this.$store.dispatch('actionInput', { inputValue })
        }
      }
    }
  }
</script>

Для целей этого теста нам всё равно, что делает действие или как выглядит структура хранилища. Мы должны просто узнать, что это действие вызывается когда должно, и что оно вызывается с ожидаемым значением.

Чтобы протестировать это, нам нужно передать мок хранилища в Vue, когда мы отделяем наш компонент.

Вместо передачи хранилища в базовый конструктор Vue, мы можем передать его в localVue. localVue — это изолированный конструктор Vue, в который мы можем вносить изменения без влияния на глобальный конструктор Vue.

Давайте посмотрим, как это выглядит:

import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import Actions from '../../../src/components/Actions'

const localVue = createLocalVue()

localVue.use(Vuex)

describe('Actions.vue', () => {
  let actions
  let store

  beforeEach(() => {
    actions = {
      actionClick: jest.fn(),
      actionInput: jest.fn()
    }
    store = new Vuex.Store({
      actions
    })
  })

  it('вызывает "actionInput", когда значение события — "input"', () => {
    const wrapper = shallowMount(Actions, { store, localVue })
    const input = wrapper.find('input')
    input.element.value = 'input'
    input.trigger('input')
    expect(actions.actionInput).toHaveBeenCalled()
  })

  it('не вызывает "actionInput", когда значение события не "input"', () => {
    const wrapper = shallowMount(Actions, { store, localVue })
    const input = wrapper.find('input')
    input.element.value = 'not input'
    input.trigger('input')
    expect(actions.actionInput).not.toHaveBeenCalled()
  })

  it('вызывает действие хранилища "actionClick" по нажатию кнопки', () => {
    const wrapper = shallowMount(Actions, { store, localVue })
    wrapper.find('button').trigger('click')
    expect(actions.actionClick).toHaveBeenCalled()
  })
})

Что тут происходит? Сначала мы указываем Vue использовать Vuex с помощью метода localVue.use. Это всего лишь обёртка вокруг Vue.use.

Затем мы создаём мок хранилища вызовом new Vuex.store с нашими заготовленными значениями. Мы передаём ему только действия, так как это всё, что нам необходимо.

Действия реализуются с помощью mock-функций jest. Эти mock-функции предоставляют нам методы для проверки, вызывались ли действия или нет.

Затем мы можем проверить в наших тестах, что заглушка действия была вызвана когда ожидалось.

Теперь способ, которым мы определяем наше хранилище выглядит немного необычным для вас.

Мы используем beforeEach, чтобы убедиться, что у нас есть чистое хранилище перед каждым тестом. beforeEach — это хук в mocha, который вызывается перед каждым тестом. В нашем тесте мы переназначаем значения переменных хранилища. Если бы мы этого не сделали, mock-функции нужно было бы автоматически сбрасывать. Это также позволяет нам изменять состояние в наших тестах, не влияя на последующие тесты.

Самое важно, что следует отметить в этом тесте — то что мы создаём мок хранилища Vuex и затем передаём его в vue-test-utils.

Отлично, теперь мы можем создавать моки действий, давайте посмотрим на создание моков для геттеров.

Создание моков для геттеров

<template>
  <div>
    <p v-if="inputValue">{{inputValue}}</p>
    <p v-if="clicks">{{clicks}}</p>
  </div>
</template>

<script>
  import { mapGetters } from 'vuex'

  export default {
    computed: mapGetters(['clicks', 'inputValue'])
  }
</script>

Это довольно простой компонент. Он отображает результат геттеров clicks и inputValue. Опять же, нас не волнует что возвращают эти геттеры — только то, что их результат будет корректно отображён.

Давайте посмотрим на тест:

import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import Getters from '../../../src/components/Getters'

const localVue = createLocalVue()

localVue.use(Vuex)

describe('Getters.vue', () => {
  let getters
  let store

  beforeEach(() => {
    getters = {
      clicks: () => 2,
      inputValue: () => 'input'
    }

    store = new Vuex.Store({
      getters
    })
  })

  it('Отображает "state.inputValue" в первом теге p', () => {
    const wrapper = shallowMount(Getters, { store, localVue })
    const p = wrapper.find('p')
    expect(p.text()).toBe(getters.inputValue())
  })

  it('Отображает "state.clicks" во втором теге p', () => {
    const wrapper = shallowMount(Getters, { store, localVue })
    const p = wrapper.findAll('p').at(1)
    expect(p.text()).toBe(getters.clicks().toString())
  })
})

Этот тест очень похож на тест действий. Мы создаём мок хранилища перед каждым тестом, передаём его в качестве опции когда вызываем shallowMount, и проверяем что значение вернувшееся из мока-геттера отображается.

Это здорово, но что, если мы хотим проверить, что наши геттеры возвращают корректную часть нашего состояния?

Создание моков с модулями

Модули полезны для разделения нашего хранилища на управляемые части. Они также экспортируют геттеры. Мы можем использовать их в наших тестах.

Давайте взглянем на наш компонент:

<template>
  <div>
    <button @click="moduleActionClick()">Нажми</button>
    <p>{{moduleClicks}}</p>
  </div>
</template>

<script>
  import { mapActions, mapGetters } from 'vuex'

  export default {
    methods: {
      ...mapActions(['moduleActionClick'])
    },

    computed: mapGetters(['moduleClicks'])
  }
</script>

Простой компонент, который содержит одно действие и один геттер.

И тест:

import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import MyComponent from '../../../src/components/MyComponent'
import myModule from '../../../src/store/myModule'

const localVue = createLocalVue()

localVue.use(Vuex)

describe('MyComponent.vue', () => {
  let actions
  let state
  let store

  beforeEach(() => {
    state = {
      clicks: 2
    }

    actions = {
      moduleActionClick: jest.fn()
    }

    store = new Vuex.Store({
      modules: {
        myModule: {
          state,
          actions,
          getters: myModule.getters
        }
      }
    })
  })

  it('вызывает действие "moduleActionClick" при нажатии кнопки', () => {
    const wrapper = shallowMount(MyComponent, { store, localVue })
    const button = wrapper.find('button')
    button.trigger('click')
    expect(actions.moduleActionClick).toHaveBeenCalled()
  })

  it('отображает "state.inputValue" в первом теге p', () => {
    const wrapper = shallowMount(MyComponent, { store, localVue })
    const p = wrapper.find('p')
    expect(p.text()).toBe(state.clicks.toString())
  })
})

Тестирование хранилища Vuex

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

Чтобы понять, как протестировать хранилище Vuex, мы создадим простое хранилище-счётчик. В хранилище есть мутация increment и геттер evenOrOdd.

// mutations.js
export default {
  increment(state) {
    state.count++
  }
}
// getters.js
export default {
  evenOrOdd: state => (state.count % 2 === 0 ? 'even' : 'odd')
}

Тестирование геттеров, мутаций и действий отдельно

Геттеры, мутации и действия — JavaScript-функции, поэтому мы можем протестировать их без использования Vue Test Utils и Vuex.

Преимущество тестирования геттеров, мутаций и действий по отдельности заключается в том, как ваши модульные тесты подробно описаны. Когда они терпят неудачу, вы точно знаете, что не так с вашим кодом. Недостатком является то, что вы нужны моки функций Vuex, таких как commit и dispatch. Это может привести к ситуации, когда модульные тесты проходят, но production-код терпит неудачу, потому что моки некорректные.

Мы создадим два тестовых файла: mutations.spec.js и getters.spec.js:

Сначала давайте протестируем мутации increment:

// mutations.spec.js

import mutations from './mutations'

test('мутация "increment" увеличивает "state.count" на 1', () => {
  const state = {
    count: 0
  }
  mutations.increment(state)
  expect(state.count).toBe(1)
})

Теперь давайте протестируем геттер evenOrOdd. Мы можем протестировать его, путём создания мока для state, вызвав геттер с state и проверкой, что возвращается корректное значение.

// getters.spec.js

import getters from './getters'

test('evenOrOdd возвращает even, если в state.count находится even', () => {
  const state = {
    count: 2
  }
  expect(getters.evenOrOdd(state)).toBe('even')
})

test('evenOrOdd возвращает odd, если в state.count находится odd', () => {
  const state = {
    count: 1
  }
  expect(getters.evenOrOdd(state)).toBe('odd')
})

Тестирование запущенного хранилища

Другой подход к тестированию хранилища Vuex — это создание запущенного хранилища с использованием конфигурации хранилища.

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

Недостатком является то, что если тест ломается, может быть трудно найти, в чём проблема.

Давайте напишем тест. Когда мы создаём, мы будем использовать localVue, чтобы избежать загрязнения базового конструктора Vue. Тест создаёт хранилище, используя экспорт store-config.js:

// store-config.js

import mutations from './mutations'
import getters from './getters'

export default {
  state: {
    count: 0
  },
  mutations,
  getters
}
// store-config.spec.js

import { createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import storeConfig from './store-config'
import { cloneDeep } from 'lodash'

test('инкрементирует значение счётчика, когда происходит инкремент', () => {
  const localVue = createLocalVue()
  localVue.use(Vuex)
  const store = new Vuex.Store(cloneDeep(storeConfig))
  expect(store.state.count).toBe(0)
  store.commit('increment')
  expect(store.state.count).toBe(1)
})

test('обновляет геттер evenOrOdd, когда происходит инкремент', () => {
  const localVue = createLocalVue()
  localVue.use(Vuex)
  const store = new Vuex.Store(cloneDeep(storeConfig))
  expect(store.getters.evenOrOdd).toBe('even')
  store.commit('increment')
  expect(store.getters.evenOrOdd).toBe('odd')
})

Обратите внимание, что мы используем cloneDeep для клонирования конфигурации хранилища перед созданием хранилища с ним. Это связано с тем, что Vuex мутирует объект с опциями, используемый для создания хранилища. Чтобы убедиться, у нас есть пустое хранилище в каждом тесте, нам нужно клонировать объект storeConfig.

Ресурсы