Проверка вызова hooks в правильном порядке

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


Основные хуки жизненного цикла

Vue предоставляет несколько жизненных хуков, на которые ориентируются при тестировании:

  • beforeCreate — вызывается сразу после инициализации экземпляра, до создания реактивных свойств.
  • created — происходит после создания реактивных свойств, но до монтирования DOM.
  • beforeMount — вызывается перед монтированием компонента в DOM.
  • mounted — вызывается после того, как компонент смонтирован в DOM.
  • beforeUpdate — вызывается перед обновлением реактивных данных и перерисовкой.
  • updated — вызывается после обновления данных и перерисовки компонента.
  • beforeUnmount — вызывается перед уничтожением компонента.
  • unmounted — вызывается после уничтожения компонента.

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


Использование Vue Test Utils для проверки порядка

Библиотека Vue Test Utils предоставляет несколько способов отслеживания вызовов хуков:

  1. Шпион на метод компонента или хук
import { mount } from '@vue/test-utils';
import MyComponent from '@/components/MyComponent.vue';

jest.spyOn(MyComponent.methods, 'myMethod');

const wrapper = mount(MyComponent);

expect(MyComponent.methods.myMethod).toHaveBeenCalled();

Для жизненных хуков Vue 3 применяются спай функции:

const beforeMountSpy = jest.fn();
const mountedSpy = jest.fn();

const wrapper = mount({
  template: '<div></div>',
  beforeMount() { beforeMountSpy(); },
  mounted() { mountedSpy(); },
});

expect(beforeMountSpy).toHaveBeenCalledBefore(mountedSpy);

В данном примере важно не только зафиксировать факт вызова, но и последовательность через toHaveBeenCalledBefore.


Проверка последовательности с асинхронными операциями

Асинхронные действия могут смещать порядок вызовов, особенно если используется nextTick или setTimeout. Для корректного тестирования используют асинхронные ожидания:

import { nextTick } from 'vue';

const updatedSpy = jest.fn();

const wrapper = mount({
  data() {
    return { count: 0 };
  },
  updated() {
    updatedSpy();
  },
  template: '<div>{{ count }}</div>',
});

wrapper.vm.count++;
await nextTick();

expect(updatedSpy).toHaveBeenCalled();

await nextTick() гарантирует, что все реактивные изменения завершены, и хук updated сработал корректно.


Отслеживание нескольких хуков одновременно

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

const callOrder = [];

const wrapper = mount({
  template: '<div></div>',
  beforeCreate() { callOrder.push('beforeCreate'); },
  created() { callOrder.push('created'); },
  beforeMount() { callOrder.push('beforeMount'); },
  mounted() { callOrder.push('mounted'); },
});

expect(callOrder).toEqual([
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted'
]);

Этот метод позволяет тестировать полный порядок вызовов, включая потенциальные побочные эффекты, связанные с инициализацией данных и DOM.


Особенности тестирования хуков в функциональных и дочерних компонентах

  • Функциональные компоненты не имеют состояния и жизненного цикла, но могут вызывать композиционные хуки, такие как onMounted. В этом случае спай применяется к самим хукам внутри setup.
import { onMounted } from 'vue';

const onMounted Spy = jest.fn();

mount({
  setup() {
    onMounted(onMountedSpy);
  },
  template: '<div></div>'
});

expect(onMountedSpy).toHaveBeenCalled();
  • Дочерние компоненты могут вызывать хуки раньше или позже родительских в зависимости от порядка рендеринга. Для тестов полезно логировать вызовы в массив и анализировать полную последовательность.

Практические рекомендации

  1. Использовать jest.fn() или vi.fn() для отслеживания вызовов хуков.
  2. Применять await nextTick() при тестировании хуков, связанных с обновлением DOM или реактивных данных.
  3. Логировать порядок вызовов для сложных компонентов с несколькими хуками.
  4. Проверять не только факт вызова, но и правильный порядок через массивы или специализированные matchers (toHaveBeenCalledBefore).
  5. Для хуков в setup использовать композиционные функции (onMounted, onUpdated) и спай функции внутри setup.

Проверка пользовательских hooks

Композиционные хуки (custom hooks) часто объединяют несколько жизненных циклов и реактивных состояний. Для проверки порядка их вызова создают обёртки, где каждый внутренний вызов логируется:

function useCustomHook() {
  const calls = [];
  onMounted(() => calls.push('mounted'));
  onUpdated(() => calls.push('updated'));
  return calls;
}

const wrapper = mount({
  setup() {
    const calls = useCustomHook();
    return { calls };
  },
  template: '<div></div>'
});

await wrapper.vm.$nextTick();
expect(wrapper.vm.calls).toContain('mounted');

Такой подход позволяет контролировать последовательность вызовов внутри сложных хук-комбинаций.


Взаимодействие с библиотеками сторонних компонентов

При работе с библиотеками, использующими собственные хуки (например, Vuetify, Element Plus), важно проверять, что пользовательские хуки вызываются после необходимых библиотечных инициализаций. Для этого применяются логирующие спаи и массивы вызовов с последующей проверкой порядка.


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