Тестирование watch и watchEffect внутри composables

Особенности работы watch и watchEffect

В Vue 3 watch и watchEffect являются основными инструментами реактивного наблюдения за состоянием. watch отслеживает конкретные реактивные источники, тогда как watchEffect автоматически отслеживает все реактивные зависимости, использованные внутри колбэка. При тестировании composables важно учитывать:

  • Асинхронность обновлений реактивного состояния — изменения в реактивных переменных не происходят мгновенно, они ставятся в очередь обновлений. Это требует использования nextTick или асинхронных ожиданий (await) в тестах.
  • Отдельная изоляция composable — тесты должны проверять именно поведение composable, без вмешательства внешних компонентов. Для этого используется mount или shallowMount для обертки небольшого хука внутри тестового компонента.

Подготовка окружения для тестирования

Для тестирования composables с watch и watchEffect часто создается минимальный тестовый компонент:

import { defineComponent } from 'vue';
import { mount } from '@vue/test-utils';
import useExample from '@/composables/useExample';

const TestComponent = defineComponent({
  template: `<div></div>`,
  setup() {
    return useExample();
  }
});

Использование mount позволяет watchEffect корректно срабатывать, так как Vue требует реактивного контекста компонента для запуска эффектов.

Тестирование watch

watch реагирует на изменения конкретных реактивных источников. Основные шаги:

  1. Создание реактивного источника в composable (ref или reactive).
  2. Определение колбэка watch, который выполняет необходимые действия при изменении источника.
  3. В тесте изменяется источник и проверяется, что колбэк сработал корректно.

Пример:

import { ref, watch } from 'vue';

export default function useCounter() {
  const count = ref(0);
  const doubled = ref(0);

  watch(count, (newValue) => {
    doubled.value = newValue * 2;
  });

  return { count, doubled };
}

Тест:

import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import useCounter from '@/composables/useCounter';
import { defineComponent } from 'vue';

const TestComponent = defineComponent({
  template: '<div></div>',
  setup() {
    return useCounter();
  }
});

test('watch correctly doubles count', async () => {
  const wrapper = mount(TestComponent);
  const { count, doubled } = wrapper.vm;

  count.value = 5;
  await nextTick();

  expect(doubled.value).toBe(10);
});

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

Тестирование watchEffect

watchEffect запускается сразу при создании и реагирует на все используемые реактивные зависимости. В тестах важно:

  • Проверять начальное выполнение эффекта.
  • Проверять срабатывание при изменении реактивных источников.

Пример:

import { ref, watchEffect } from 'vue';

export default function useLogger() {
  const message = ref('');
  const log = ref([]);

  watchEffect(() => {
    if (message.value) {
      log.value.push(message.value);
    }
  });

  return { message, log };
}

Тест:

import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import useLogger from '@/composables/useLogger';
import { defineComponent } from 'vue';

const TestComponent = defineComponent({
  template: '<div></div>',
  setup() {
    return useLogger();
  }
});

test('watchEffect logs message updates', async () => {
  const wrapper = mount(TestComponent);
  const { message, log } = wrapper.vm;

  expect(log.value).toEqual([]);

  message.value = 'Hello';
  await nextTick();

  expect(log.value).toEqual(['Hello']);

  message.value = 'World';
  await nextTick();

  expect(log.value).toEqual(['Hello', 'World']);
});

Особенность watchEffect: эффект запускается сразу при монтировании, поэтому начальные проверки должны учитывать это поведение.

Тонкости и подводные камни

  • watch с опцией { immediate: true } выполняет колбэк при инициализации, что нужно учитывать при тестировании начального состояния.
  • Для массивов и объектов, реактивность в watch работает только на изменения самих ссылок, а не глубоких свойств, если не указан параметр { deep: true }.
  • watchEffect автоматически отслеживает все реактивные свойства, используемые внутри, включая вложенные объекты, что упрощает тестирование сложных composables.
  • Асинхронные операции внутри watch или watchEffect требуют использования flushPromises или await nextTick(), чтобы дождаться завершения эффекта перед проверкой результата.

Интеграция с Jest и Vue Test Utils

  • Использование mount или shallowMount позволяет полностью протестировать эффекты composables в реактивном контексте.
  • Для имитации времени можно использовать jest.useFakeTimers() и jest.advanceTimersByTime(), если watch или watchEffect содержит setTimeout или подобные задержки.
  • Проверка побочных эффектов (console.log, API вызовы, изменение других реактивных переменных) выполняется через spy-функции (jest.fn()).

Практические советы

  • Для сложных composables лучше создавать отдельный минимальный компонент для тестирования, чтобы не смешивать с другими компонентами.
  • Всегда использовать await nextTick() после изменения реактивного источника перед проверкой состояния.
  • Проверять как начальное значение, так и изменения после обновлений.
  • Для глубоких наблюдений использовать { deep: true } в watch, чтобы корректно отлавливать изменения вложенных свойств.

Тщательное тестирование watch и watchEffect позволяет обнаружить ошибки реактивности на ранней стадии, обеспечивает предсказуемость поведения composables и предотвращает неожиданные побочные эффекты при масштабировании приложений.