Тестирование состояния приложения

Понимание состояния приложения

В Vue.js состояние приложения определяется как совокупность всех данных, которые влияют на отображение компонентов и поведение интерфейса. Управление состоянием может осуществляться локально в отдельных компонентах через data и реактивные свойства, либо централизованно с помощью Vuex или Pinia. Тестирование состояния требует понимания, какие данные являются критичными и как изменения этих данных отражаются на пользовательском интерфейсе.

Ключевые аспекты состояния:

  • Реактивность: Vue автоматически отслеживает изменения в данных и обновляет DOM. Тестирование должно учитывать реактивные зависимости.
  • Локальное vs глобальное состояние: Локальные данные компонента проще тестировать через юнит-тесты, тогда как глобальное состояние требует интеграционного подхода.
  • Предсказуемость: Любое изменение состояния должно быть предсказуемым и контролируемым через мутации (Vuex) или действия (actions).

Юнит-тестирование компонентов

Юнит-тестирование направлено на проверку корректного функционирования отдельных компонентов и их реактивных данных. В Vue 3 часто используют @vue/test-utils и Jest или Vitest.

Пример структуры теста для компонента с состоянием:

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

describe('Counter.vue', () => {
  it('увеличивает значение при клике', async () => {
    const wrapper = mount(Counter)
    expect(wrapper.text()).toContain('0')

    const button = wrapper.find('button')
    await button.trigger('click')

    expect(wrapper.text()).toContain('1')
  })
})

Основные принципы:

  • Проверка начального состояния через data() или computed.
  • Симуляция событий с использованием trigger.
  • Проверка реактивных изменений в DOM.
  • Изоляция компонента от внешних зависимостей.

Тестирование глобального состояния

Для приложений с Vuex или Pinia проверка состояния часто включает тестирование мутаций, действий и геттеров.

Пример тестирования мутации Vuex:

import { mutations } from '@/store'

const { increment } = mutations

describe('mutations', () => {
  it('увеличивает счетчик', () => {
    const state = { count: 0 }
    increment(state)
    expect(state.count).toBe(1)
  })
})

Принципы тестирования:

  • Мутации проверяются напрямую с имитацией состояния.
  • Действия тестируются через мокированные API-запросы.
  • Геттеры проверяются на корректное вычисление данных на основе состояния.

Пример тестирования действия с асинхронным запросом:

import { actions } from '@/store'
import axios from 'axios'
import { vi } from 'vitest'

vi.mock('axios')

describe('actions', () => {
  it('загружает данные и коммитит мутацию', async () => {
    const commit = vi.fn()
    axios.get.mockResolvedValue({ data: { count: 5 } })

    await actions.fetchCount({ commit })

    expect(commit).toHaveBeenCalledWith('setCount', 5)
  })
})

Интеграционное тестирование состояния

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

Пример подхода:

  • Использовать тестовую версию хранилища.
  • Монтировать родительский компонент с дочерними.
  • Симулировать события и проверять обновления состояния и DOM.
import { mount } from '@vue/test-utils'
import ParentComponent from '@/components/ParentComponent.vue'
import { createStore } from 'vuex'

const store = createStore({
  state: { count: 0 },
  mutations: { increment(state) { state.count++ } }
})

const wrapper = mount(ParentComponent, {
  global: { plugins: [store] }
})

// Симуляция события, которое вызывает мутацию
await wrapper.find('button.increment').trigger('click')

// Проверка обновленного состояния и отображения
expect(store.state.count).toBe(1)
expect(wrapper.text()).toContain('1')

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

Компоненты часто используют computed свойства и наблюдатели (watch). Для тестирования их поведения важно проверять:

  • Корректность вычислений при изменении входных данных.
  • Влияние на рендер компонента.
  • Реакцию на асинхронные изменения.
it('computed fullName обновляется при изменении firstName', async () => {
  const wrapper = mount({ 
    data: () => ({ firstName: 'John', lastName: 'Doe' }),
    computed: {
      fullName() { return `${this.firstName} ${this.lastName}` }
    },
    template: `<div>{{ fullName }}</div>`
  })

  expect(wrapper.text()).toBe('John Doe')
  wrapper.vm.firstName = 'Jane'
  await wrapper.vm.$nextTick()
  expect(wrapper.text()).toBe('Jane Doe')
})

Использование моков и стабов

Тестирование состояния требует контроля внешних зависимостей. Для этого применяются:

  • Моки API-запросов для имитации сетевых ответов.
  • Стабы функций и сервисов, чтобы избежать непредсказуемого поведения.
  • Подмены модулей Vuex/Pinia для изоляции тестов.

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

  • Всегда проверять начальное состояние и ожидаемые изменения.
  • Разделять тесты на юнит, интеграцию и e2e для разных уровней контроля состояния.
  • Использовать await nextTick() для корректного отслеживания реактивных изменений.
  • Минимизировать зависимости между тестами для предотвращения побочных эффектов.
  • Документировать ключевые состояния и сценарии, чтобы покрытие тестами было полным.

Тестирование состояния во Vue.js является фундаментом надежного и предсказуемого приложения. Оно охватывает как простые реактивные свойства компонентов, так и сложные потоки данных в глобальном хранилище, обеспечивая стабильность пользовательского интерфейса при изменении данных.