Mock composables для изоляции компонента

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

Принципы мокирования composables

  1. Изоляция логики Основная цель мокирования — отделить компонент от реальной логики composable, сохранив при этом интерфейс. Компонент должен продолжать вызывать функции и реактивные данные, но их значения контролируются тестом.

  2. Стабильность тестов Моки позволяют создавать повторяемые сценарии. Независимо от состояния API или стороннего сервиса, результат выполнения функции в тесте будет предсказуемым.

  3. Контроль реактивности Composables часто возвращают ref, reactive или вычисляемые свойства (computed). Мок должен поддерживать реактивность, чтобы изменения значений внутри теста корректно обновляли компонент.

Подходы к мокированию

1. Мок через Jest jest.mock()

Для composables, экспортируемых как модули, можно использовать встроенные возможности Jest:

// useUser.js
import { ref } from 'vue';

export function useUser() {
  const user = ref(null);
  
  function fetchUser() {
    user.value = { id: 1, name: 'Alice' };
  }

  return { user, fetchUser };
}

В тесте:

import { mount } from '@vue/test-utils';
import MyComponent from '@/components/MyComponent.vue';
import * as userComposable from '@/composables/useUser';

jest.mock('@/composables/useUser', () => ({
  useUser: jest.fn(),
}));

test('отображает имя пользователя', async () => {
  const mockUser = { id: 1, name: 'Mocked User' };
  userComposable.useUser.mockReturnValue({
    user: ref(mockUser),
    fetchUser: jest.fn(),
  });

  const wrapper = mount(MyComponent);
  expect(wrapper.text()).toContain('Mocked User');
});

Ключевые моменты:

  • Используется jest.mock() для перехвата импорта composable.
  • Возвращается объект с теми же ключами (user, fetchUser), что и оригинальный composable.
  • Можно контролировать значения реактивных данных (ref) для проверки поведения компонента.
2. Передача зависимостей через параметры

Иногда composable можно проектировать так, чтобы его зависимости можно было передавать извне:

export function useApi(fetchFn) {
  const data = ref(null);
  
  async function load() {
    data.value = await fetchFn();
  }

  return { data, load };
}

В тесте:

const mockFetch = jest.fn().mockResolvedValue({ id: 1 });
const { data, load } = useApi(mockFetch);
await load();
expect(data.value.id).toBe(1);

Такой подход уменьшает необходимость глобального мокирования и делает тесты более явными.

3. Использование provide/inject для composables

Если composable использует inject для получения зависимостей, их можно легко заменять через provide в тесте:

// useSettings.js
import { inject } from 'vue';

export function useSettings() {
  const settings = inject('settings', { theme: 'light' });
  return { settings };
}

Тест:

import { mount } from '@vue/test-utils';
import MyComponent from '@/components/MyComponent.vue';
import { h, provide } from 'vue';

const wrapper = mount({
  setup() {
    provide('settings', { theme: 'dark' });
    return () => h(MyComponent);
  }
});
expect(wrapper.html()).toContain('dark');

Особенности реактивных моков

  • Использовать ref и reactive внутри мока обязательно, если компонент ожидает реактивные объекты.
  • Изменение значений мока должно автоматически обновлять компонент, что позволяет тестировать реактивное поведение.
  • Можно комбинировать jest-функции и реактивные объекты для отслеживания вызовов и изменений.

Рекомендации по структуре мока

  1. Сохранять интерфейс composable — ключи и методы должны совпадать с оригиналом.
  2. Разделять данные и методы — возвращать реактивные данные отдельно от функций.
  3. Использовать Jest spy или mock функций для проверки вызовов методов.
  4. Стараться мокировать только то, что нужно для теста — избыточное мокирование усложняет поддержку.

Примеры сложных сценариев

  1. Асинхронные composables с загрузкой данных

    • Мок возвращает ref с начальными значениями.
    • Методы загрузки заменяются jest-функциями, которые изменяют ref через nextTick.
  2. Composables с подписками или watch

    • Мок может имитировать события и изменять реактивные значения, чтобы проверить реакцию компонента.
    • Можно использовать flushPromises для обработки асинхронных изменений.
  3. Composables с зависимостями от Vue Router или Pinia

    • Для Pinia: создается мок-хранилище или используется setActivePinia(createPinia()).
    • Для Router: можно заменить useRoute и useRouter на мок-объекты.

Использование моков для composables обеспечивает высокую изоляцию тестов и позволяет полностью контролировать сценарии без зависимости от внешних факторов, сохраняя реактивность и интерфейс компонента. Такой подход делает юнит-тестирование Vue 3 компонентов предсказуемым, безопасным и масштабируемым.