Тестирование поведения, а не внутреннего состояния

В тестировании компонентов Vue критически важно отличать проверку внутреннего состояния от проверки поведения. Vue Test Utils предоставляет инструменты, которые позволяют сосредоточиться на том, что компонент делает, а не на том, как он это реализует внутри. Такой подход делает тесты более устойчивыми к изменениям реализации и повышает их читаемость.


Основные принципы тестирования поведения

  1. Изоляция от реализации Проверка поведения подразумевает, что тесты не должны зависеть от внутренней структуры компонента: приватных методов, локального состояния (data) или деталей жизненного цикла. Вместо этого внимание уделяется:

    • Рендерингу DOM;
    • Событиям и их последствиям;
    • Взаимодействию с дочерними компонентами через props и события.
  2. Сценарии пользователя Тестирование строится вокруг того, как компонент ведёт себя снаружи, например:

    • Что происходит при клике на кнопку;
    • Как изменяется отображение при получении props;
    • Какие события эмитятся в ответ на действия пользователя.
  3. Минимизация связей с внутренними данными Прямой доступ к wrapper.vm.$data или приватным методам считается плохой практикой, так как изменения внутри компонента не должны ломать тесты.


Работа с событиями

Одной из основных задач является проверка реакции компонента на пользовательские действия.

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

test('увеличивает счетчик при клике', async () => {
  const wrapper = mount(Counter)
  
  await wrapper.find('button.increment').trigger('click')
  
  expect(wrapper.text()).toContain('1')
})

Объяснение:

  • trigger('click') имитирует событие пользователя.
  • Проверка текста wrapper.text() подтверждает видимое поведение, а не внутреннюю переменную count.

Проверка эмита событий

Компоненты часто взаимодействуют через кастомные события. В Vue Test Utils это проверяется через emitted():

test('отправляет событие submit при отправке формы', async () => {
  const wrapper = mount(FormComponent)
  
  await wrapper.find('form').trigger('submit.prevent')
  
  expect(wrapper.emitted()).toHaveProperty('submit')
  expect(wrapper.emitted('submit')[0]).toEqual([{ username: 'test' }])
})

Ключевой момент: тест проверяет факт эмита события и его полезную нагрузку, не заглядывая внутрь метода, который это событие генерирует.


Тестирование реактивного отображения

Иногда компонент меняет внешний вид в зависимости от props. Проверка поведения фокусируется на видимых изменениях.

test('отображает сообщение об ошибке при ошибочном prop', () => {
  const wrapper = mount(AlertComponent, {
    props: { type: 'error', message: 'Ошибка!' }
  })
  
  expect(wrapper.classes()).toContain('alert-error')
  expect(wrapper.text()).toBe('Ошибка!')
})

Здесь нет необходимости проверять data или computed — тест подтверждает, что поведение DOM соответствует props.


Сравнение с тестированием состояния

Тестирование состояния Тестирование поведения
Проверка wrapper.vm.count Проверка, что при клике кнопки текст увеличился
Проверка приватных методов Проверка эмита события или видимого эффекта
Хрупкость при рефакторинге Устойчивость к изменениям реализации

Работа с дочерними компонентами

При проверке поведения взаимодействие с дочерними компонентами лучше имитировать через stubs или проверку эмитированных событий:

const wrapper = mount(ParentComponent, {
  global: {
    stubs: ['ChildComponent']
  }
})

await wrapper.findComponent({ name: 'ChildComponent' }).vm.$emit('custom-event')

expect(wrapper.emitted('child-event')).toBeTruthy()

Тест фиксирует результат взаимодействия, а не внутренние методы дочернего компонента.


Асинхронное поведение

Для асинхронных действий, таких как запросы API или таймеры, проверка должна фиксировать результат, а не промежуточные состояния:

test('показывает данные после загрузки', async () => {
  const wrapper = mount(DataLoader, { props: { url: '/api/data' } })
  
  await wrapper.vm.$nextTick()
  
  expect(wrapper.text()).toContain('Данные загружены')
})
  • await wrapper.vm.$nextTick() обеспечивает завершение реактивного обновления DOM.
  • Важен результат, а не внутренний data.items.

Выводы по стратегии

  • Тесты, сфокусированные на поведении, легче читать и поддерживать.
  • Они не ломаются при изменении внутренних методов или структуры data.
  • Проверка должна происходить через интерфейс компонента: DOM, события, props, слоты.

Такой подход превращает Vue Test Utils в инструмент для проверки пользовательского опыта и корректного взаимодействия компонентов, а не просто в средство инспекции состояния.