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

Composables представляют собой функции, использующие Composition API для инкапсуляции реактивной логики и повторного её использования между компонентами. Тестирование composables отличается от тестирования компонентов, так как нет необходимости рендерить DOM, но важно правильно обрабатывать реактивность, эффекты и взаимодействие с другими composables или API.

Установка инструментов

Для тестирования composables используются:

  • Vue Test Utils — для работы с компонентами и wrapper-объектами.
  • @vue/reactivity или встроенные функции Vue (ref, reactive, computed) — для работы с реактивностью.
  • Jest / Vitest — тестовый раннер для выполнения тестов и мокирования зависимостей.

Пример установки через npm:

npm install @vue/test-utils @vue/reactivity vitest

Структура composable

Типичный composable может выглядеть так:

import { ref, computed } from 'vue';

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

  const increment = () => {
    count.value += 1;
  };

  const decrement = () => {
    count.value -= 1;
  };

  const double = computed(() => count.value * 2);

  return { count, increment, decrement, double };
}

Главные элементы, на которые обращаем внимание при тестировании:

  • Реактивные переменные (ref, reactive)
  • Вычисляемые свойства (computed)
  • Функции, изменяющие состояние
  • Побочные эффекты (watchers, watchEffect, lifecycle hooks внутри composables)

Тестирование composables без компонента

Vue Test Utils предоставляет возможность рендерить composables напрямую через mount компонента-обертки или через специализированные хелперы.

Простейший способ — создать минимальный компонент для использования composable:

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

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

Теперь можно тестировать функциональность composable через экземпляр компонента:

test('increment увеличивает count', () => {
  const wrapper = mount(TestComponent);
  const { count, increment } = wrapper.vm;

  expect(count).toBe(0);
  increment();
  expect(count).toBe(1);
});

Использование хелперов setup() напрямую

Для упрощенного тестирования без рендера можно использовать функции типа renderComposable из сторонних библиотек (например, @vue/test-utils не имеет встроенной функции renderComposable, но можно реализовать обертку через mount), либо писать собственные утилиты:

import { reactive, nextTick } from 'vue';

function renderComposable(callback) {
  let state;
  const TestComponent = defineComponent({
    setup() {
      state = callback();
      return state;
    },
    template: '<div></div>'
  });
  mount(TestComponent);
  return state;
}

Пример использования:

const { count, increment } = renderComposable(() => useCounter(5));
expect(count.value).toBe(5);
increment();
expect(count.value).toBe(6);

Тестирование реактивности и computed

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

test('computed double корректно обновляется', async () => {
  const { count, increment, double } = renderComposable(() => useCounter(2));

  expect(double.value).toBe(4);
  increment();
  await nextTick();
  expect(double.value).toBe(6);
});

Использование nextTick() гарантирует, что Vue обработает реактивные обновления перед проверкой результата.

Тестирование побочных эффектов

Если composable использует watch или watchEffect, необходимо правильно мокировать функции и проверять вызовы:

import { watch } from 'vue';

function useWatcherDemo() {
  const count = ref(0);
  const spy = jest.fn();

  watch(count, spy);

  const increment = () => count.value++;
  return { count, increment, spy };
}

test('watch срабатывает при изменении count', async () => {
  const { count, increment, spy } = renderComposable(() => useWatcherDemo());

  increment();
  await nextTick();
  expect(spy).toHaveBeenCalledWith(1, 0);
});

Мокирование зависимостей

Composables часто используют внешние API или другие composables. Для тестов рекомендуется мокировать зависимости, чтобы тест был изолированным и детерминированным:

jest.mock('@/composables/useApi', () => ({
  useApi: () => ({
    fetchData: jest.fn().mockResolvedValue('mocked data')
  })
}));

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

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

  • Всегда проверять начальные значения реактивных переменных.
  • Разделять тесты на функциональные группы: изменение состояния, computed свойства, watch эффекты.
  • Использовать nextTick() или await wrapper.vm.$nextTick() при проверке реактивных обновлений.
  • Мокировать внешние API для изоляции тестов.
  • Проверять побочные эффекты и возврат значений из composable, особенно если используются асинхронные операции.

Выводы по методике

Тестирование composables требует внимания к реактивности, правильного обращения с computed и watchers, а также грамотного мокирования внешних зависимостей. Благодаря чистой функции setup и отсутствию необходимости рендерить сложный DOM, тесты composables могут быть легкими, быстрыми и точными. Это обеспечивает уверенность в корректности бизнес-логики и повторяемости поведения Vue-приложения.