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)
})
})
Что дальше
- Интегрируйте
vue-test-utils
в ваш проект выбрав программу для запуска тестов. - Прочитайте больше об общих техниках и советах при написании тестов.
Общие советы
Понимание что тестировать
Для компонентов пользовательского интерфейса мы не рекомендуем стремиться к покрытию каждой строки кода, поскольку это приводит к слишком большому фокусу на деталях внутренней реализации компонентов и может привести к созданию хрупких тестов.
Вместо этого, мы рекомендуем писать тесты, которые проверяют ваш публичный интерфейс взаимодействия с компонентом и относиться к его внутренностям как к чёрному ящику. Один тестовый пример должен проверять, что некоторые входные данные (взаимодействие пользователя или изменение входных параметров), предоставляемые компоненту будут приводить к ожидаемому результату (результату рендеринга или вызванным пользовательским событиям).
Например, для компонента 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.
router-link
или router-view
Тестирование компонентов использующих Когда вы устанавливаете 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
.