В этом руководстве мы рассмотрим как тестировать Vuex в компонентах с Vue Test Utils и как подходить к тестированию хранилища 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({
state: {},
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, мы создадим простое хранилище-счетчик. В хранилище есть мутация 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 increments state.count by 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.spec.js
import mutations from './mutations'
import getters from './getters'
export default {
state: {
count: 0
},
mutations,
getters
}
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
.