Vue - это библиотека пользовательского интерфейса, поэтому, естественно, тестирование компонентов Vue обычно включает в себя проверку того, правильно ли пользовательский интерфейс отражает состояние приложения или, в данном случае, данные, полученные из computed
, data
или в более крупных приложениях, хранилище Vuex.
Однако в большинстве случаев вы хотите _избежать_ использования Vuex в тестах компонентов. Позвольте мне объяснить, почему, и некоторые передовые практики, которые я усвоил, работая над несколькими большими приложениями Vue / Vuex.
Исходный код этой статьи находится здесь.
Тестирование компонентов, использующих $ store.state
Vuex предоставляет четыре основных API для взаимодействия с магазином - state
, mutations
, actions
и getters
. Давайте сначала посмотрим на state
.
Компонент ниже показывает три обычных способа доступа к $store.state
: напрямую с помощью {{ $store.state }}
(что мне не нравится, почему ниже), с использованием вычисляемого свойства для возврата свойства состояния или с помощью помощника mapState
.
<template> <div> <div class="state-1"> {{ $store.state.value_1 }} </div> <div class="state-2"> {{ value_2 }} </div> <div class="state-3"> {{ value_3 }} </div> </div> </template> <script> import { mapState } from 'vuex' export default { name: 'State', computed: { value_2 () { return this.$store.state.value_2 }, ...mapState({ value_3: state => state.value_3 }) } } </script>
Ниже показано, как это проверить. Вы можете понять, почему вызов {{ $store }}
непосредственно в разметке утомителен - он возвращает вас к полной имитации магазина, в которой нет необходимости утверждать, что пользовательский интерфейс ведет себя определенным образом в зависимости от состояния. При тестировании, правильно ли отображается компонент на основе некоторого состояния, нас не волнует, откуда берутся данные - только то, что пользовательский интерфейс правильно их отражает.
import Vuex from 'vuex' import { shallow, createLocalVue } from 'vue-test-utils' import State from './State.vue' const localVue = createLocalVue() localVue.use(Vuex) describe('State', () => { it('renders a value from $store.state', () => { const wrapper = shallow(State, { mocks: { $store: { state: { value_1: 'value_1' } } }, localVue }) expect(wrapper.find('.state-1') .text().trim()).toEqual('value_1') }) it('renders a $store.state value return from computed', () => { const wrapper = shallow(State, { computed: { value_2: () => 'value_2' }, localVue }) expect(wrapper.find('.state-2') .text().trim()).toEqual('value_2') }) it('renders a $store.state value return from mapState', () => { const wrapper = shallow(State, { computed: { value_3: () => 'value_3' }, localVue }) expect(wrapper.find('.state-3') .text().trim()).toEqual('value_3') }) })
Главный вывод здесь заключается в том, что вы можете имитировать данные, используя параметр computed
для shallow
и mount
.
Тестирование компонентов, в которых хранятся мутации. $ Commit
Когда пользователь взаимодействует с компонентом, который совершает мутацию, вы хотите протестировать следующие вещи:
- совершается ли мутация?
- у него правильная полезная нагрузка?
Вот и все. Компонентные тесты - не то место, где можно утверждать, что state
действительно был изменен - вы должны тестировать это изолированно.
Вот простой компонент, который совершает мутацию при нажатии кнопки:
<template> <div> <button @click="handle">Button</button> </div> </template> <script> export default { name: 'Mutations', data () { return { val: 'val' } }, methods: { handle () { this.$store.commit('MUTATION', { val: this.val }) } } } </script>
И тест.
import Vuex from 'vuex' import { shallow, createLocalVue } from 'vue-test-utils' import Mutations from './Mutations.vue' const localVue = createLocalVue() localVue.use(Vuex) describe('Mutations', () => { let store let mutations beforeEach(() => { mutations = { MUTATION: jest.fn() } store = new Vuex.Store({ mutations }) }) it('commits a MUTATION type mutation when a button is clicked', () => { const wrapper = shallow(Mutations, { store, localVue }) wrapper.find('button').trigger('click') expect(mutations.MUTATION.mock.calls).toHaveLength(1) expect(mutations.MUTATION.mock.calls[0][1]) .toEqual({ val: 'val' }) }) })
Лучше использовать реальный store
, когда подтверждены мутации, чтобы вы могли не только утверждать, что метод вызывается правильно, но и передаются правильные данные. Обратите внимание, что вы должны сделать calls[0][1]
- мутации всегда получают state
в качестве первого аргумента, а payload
- в качестве второго.
Вы можете сделать то же самое для mapCommit
, но я никогда не находил применения этому помощнику, поэтому не показывал его.
Тестирование компонентов, использующих $ store.getters
Тестировать компоненты, которые полагаются на $store.getters
, легко и похоже на $store.state
, поскольку геттеры - это в основном просто вычисляемые свойства для хранилища. Есть несколько способов сделать это. Во-первых, нижеприведенный компонент показывает два разных способа использования геттеров: вспомогательный mapGetters
, свойство computed
, которое также передает, и аргумент.
<template> <div> <div class="map-getters"> {{ getter_1 }} </div> <div class="computed-getters"> {{ getter_2 }} </div> </div> </template> <script> import { mapGetters } from 'vuex' export default { name: 'Getters', computed: { ...mapGetters({ getter_1: 'getter_1' }), getter_2 () { return this.$store.getters['getter_2']('value_2') } } } </script>
Тестирование геттеров с магазином
import Vuex from 'vuex' import { shallow, createLocalVue } from 'vue-test-utils' import Getters from './Getters.vue' const localVue = createLocalVue() localVue.use(Vuex) describe('Getters', () => { describe('with a store', () => { let store let getters beforeEach(() => { getters = { getter_1: () => 'value_1', getter_2: () => (arg) => arg } store = new Vuex.Store({ getters }) }) it('renders a values from getters', () => { const wrapper = shallow(Getters, { store, localVue }) expect(wrapper.find('.map-getters') .text().trim()).toEqual('value_1') expect(wrapper.find('.computed-getters') .text().trim()).toEqual('value_2') }) }) })
Довольно просто. Следует помнить, что вы можете создать новое хранилище, выполнив let store
вне блока describe
, а внутри beforeEach
hook создать новое хранилище. Если вы хотите изменить значение геттера для определенного теста, вы можете использовать store.hotUpdate
следующим образом:
it('shows how to use store.hotUpdate', () => { store.hotUpdate({ getters: { ...getters, getter_1: () => 'wrong_value' } }) const wrapper = shallow(Getters, { store, localVue }) expect(wrapper.find('.map-getters') .text().trim()).not.toEqual('value_1') }) })
Убедитесь, что вы используете оператор распространения ...
для передачи остальных геттеров в начальное состояние.
Однако ... это не всегда идеально по нескольким причинам. Нам не следует проверять, работают ли геттеры Vuex и mapGetters
- тестируйте свой код, а не фреймворк. Есть еще вариант:
Тестирование геттеров без магазина
describe('without a store', () => { it('renders a value from getters', () => { const wrapper = shallow(Getters, { localVue, computed: { getter_1: () => 'value_1', getter_2: () => 'value_2' } }) expect(wrapper.find('.map-getters') .text().trim()).toEqual('value_1') expect(wrapper.find('.computed-getters') .text().trim()).toEqual('value_2') }) })
getters
- это просто вычисленные значения для хранилища, которые Vuex превращает в обычные вычисляемые свойства. Основным моментом здесь является проверка того, что пользовательский интерфейс правильно представляет состояние приложения - нас не волнует, откуда берутся данные, просто то, что они представлены правильно, поэтому есть преимущество полного удаления комбо геттеров / хранилищ, и просто установить желаемые значения с помощью опции computed
.
Тестирование компонентов, выполняющих действия $ store.dispatch
Это очень похоже на тестирование $store.commit
. Опять же, мы не проверяем, что происходит после отправки действия, а просто проверяем, что это отправка и с правильной полезной нагрузкой.
Этот компонент отправляет действие, когда значение watch
изменяется, запускает отправку или когда нажимается кнопка:
<template> <div> <button @click="go" /> </div> </template> <script> export default { name: 'Actions', data () { return { val: 1 } }, methods: { go () { this.$store.dispatch('someAction', { val: this.val }) } }, watch: { 'val' (val, oldVal) { this.$store.dispatch('someAction', { val }) } } } </script>
И тест:
import Vuex from 'vuex' import { shallow, createLocalVue } from 'vue-test-utils' import Actions from './Actions.vue' const localVue = createLocalVue() localVue.use(Vuex) describe('Actions', () => { let store let actions beforeEach(() => { actions = { someAction: jest.fn() } store = new Vuex.Store({ actions }) }) it('dispatches an action when a watched value changes', () => { const wrapper = shallow(Actions, { store, localVue }) wrapper.find('button').trigger('click') expect(actions.someAction.mock.calls).toHaveLength(1) expect(actions.someAction.mock.calls[0][1]).toEqual({ val: 1 }) }) it('dispatches an action when a watched value changes', async () => { const wrapper = shallow(Actions, { store, localVue }) await wrapper.setData({ val: 2 }) expect(actions.someAction.mock.calls).toHaveLength(1) expect(actions.someAction.mock.calls[0][1]).toEqual({ val: 2 }) }) })
Обратите внимание, поскольку $watch
вызывается асинхронно, вам нужно отметить тест async
и await
вызов setData
. Однако нажатие кнопки происходит синхронно, поэтому вы можете просто написать его, как любой старый тест.
Большая часть тестирования компонентов Vue вращается вокруг этих идей - настройте данные и убедитесь, что пользовательский интерфейс правильно их представляет, или проверка события вызывает вызов какого-либо другого метода или установку некоторого значения.
Чем меньше и менее умно вы сделаете свои компоненты, тем легче их будет протестировать. У меня обычно есть несколько «умных» или «контейнерных» компонентов, которые имеют множество тестов, гарантирующих, что при взаимодействии пользователя происходят разные вещи, а затем просто передаю данные в реквизиты «тупым» или «обычным» компонентам - это самые простые для проверки, поскольку их состояние в основном зависит от props
, которое вы им передаете.
Подумайте о том, какие компоненты вам понадобятся и как вы будете их тестировать заранее, чтобы улучшить архитектуру вашего приложения и сделать его более удобным для сопровождения и тестирования в долгосрочной перспективе. Я подробнее остановлюсь на этой идее в одном из следующих постов.
Исходный код этой статьи находится здесь.