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

Когда пользователь взаимодействует с компонентом, который совершает мутацию, вы хотите протестировать следующие вещи:

  1. совершается ли мутация?
  2. у него правильная полезная нагрузка?

Вот и все. Компонентные тесты - не то место, где можно утверждать, что 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, а внутри beforeEachhook создать новое хранилище. Если вы хотите изменить значение геттера для определенного теста, вы можете использовать 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, которое вы им передаете.

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

Исходный код этой статьи находится здесь.