Сложные логики с несколькими composables

При тестировании компонентов Vue, которые используют несколько composables, важно понимать, как правильно изолировать логику и проверять её корректность без необходимости рендерить весь компонент. Composables позволяют организовать повторно используемую реактивную логику, но при их совмещении возникает несколько тонкостей, связанных с mock-ами, реактивностью и взаимодействием между ними.

Монтирование компонентов с несколькими composables

mount и shallowMount из Vue Test Utils являются основными инструментами для создания экземпляров компонентов. При тестировании компонентов с несколькими composables:

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

    import { mount } from '@vue/test-utils';
    import MyComponent from '@/components/MyComponent.vue';
    import * as useAuthComposable from '@/composables/useAuth';
    import * as useApiComposable from '@/composables/useApi';
    
    jest.mock('@/composables/useAuth');
    jest.mock('@/composables/useApi');
    
    useAuthComposable.useAuth.mockReturnValue({
      isLoggedIn: true,
      user: { name: 'Alice' }
    });
    
    useApiComposable.useApi.mockReturnValue({
      fetchData: jest.fn().mockResolvedValue([1, 2, 3])
    });
    
    const wrapper = mount(MyComponent);
  2. shallowMount для изоляции дочерних компонентов shallowMount предотвращает рендеринг дочерних компонентов, что особенно полезно при сложной логике, где composables передают данные в несколько уровней компонентов.

    import { shallowMount } from '@vue/test-utils';
    
    const wrapper = shallowMount(MyComponent);

Проверка реактивных состояний нескольких composables

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

  • Использовать nextTick для ожидания обновления реактивных данных.
  • Проверять состояние composables через возвращаемые значения или computed-свойства компонента.
import { nextTick } from 'vue';

test('компонент корректно объединяет состояния composables', async () => {
  const wrapper = mount(MyComponent);

  expect(wrapper.vm.isLoggedIn).toBe(true);
  expect(wrapper.vm.data.length).toBe(3);

  wrapper.vm.data.push(4);
  await nextTick();

  expect(wrapper.vm.data).toEqual([1, 2, 3, 4]);
});

Mocking и spying на функции внутри composables

Если composable содержит методы, которые вызывают API или изменяют глобальное состояние, их можно замокать или использовать spy, чтобы проверить вызовы и аргументы:

const fetchDataSpy = jest.fn();
useApiComposable.useApi.mockReturnValue({
  fetchData: fetchDataSpy
});

wrapper.vm.loadData();
expect(fetchDataSpy).toHaveBeenCalledWith({ page: 1 });

Тестирование реактивных эффектов и watch

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

test('watch корректно реагирует на изменения состояния', async () => {
  const wrapper = mount(MyComponent);
  wrapper.vm.filter = 'new';
  await nextTick();

  expect(wrapper.vm.filteredData).toEqual([/* ожидаемый результат */]);
});

Взаимодействие composables через provide/inject

Если composables используют provide/inject для обмена данными между компонентами, можно мокать inject или создавать тестовый провайдер:

import { provide } from 'vue';

test('компонент получает данные через provide/inject', () => {
  const wrapper = mount(MyComponent, {
    global: {
      provide: {
        apiService: {
          fetch: jest.fn().mockResolvedValue([1, 2])
        }
      }
    }
  });

  expect(wrapper.vm.data).toEqual([]);
});

Стратегии тестирования сложной логики

  1. Тестирование composable отдельно Каждый composable можно протестировать как отдельную функцию, проверяя его реактивное состояние, методы и side-effect. Это уменьшает сложность тестов компонентов.

  2. Интеграционное тестирование с компонентом После проверки composables отдельно важно протестировать их совместную работу внутри компонента. Здесь проверяются:

    • Передача данных между composables
    • Реактивные зависимости
    • Вызовы методов composables при событиях компонента
  3. Использование spy и mock для минимизации внешних зависимостей Любые обращения к API, localStorage или глобальным сервисам стоит мокать, чтобы тест оставался детерминированным.

Практический пример

Компонент Dashboard.vue использует два composables: useAuth для авторизации и useStats для получения статистики.

// useAuth.js
export function useAuth() {
  const isLoggedIn = ref(false);
  const user = ref(null);
  return { isLoggedIn, user };
}

// useStats.js
export function useStats() {
  const stats = ref([]);
  const fetchStats = async () => {
    stats.value = await fetch('/api/stats').then(res => res.json());
  };
  return { stats, fetchStats };
}

Тест для Dashboard.vue с проверкой интеграции composables:

import { mount } from '@vue/test-utils';
import Dashboard from '@/components/Dashboard.vue';
import * as useAuthComposable from '@/composables/useAuth';
import * as useStatsComposable from '@/composables/useStats';

jest.mock('@/composables/useAuth');
jest.mock('@/composables/useStats');

useAuthComposable.useAuth.mockReturnValue({ isLoggedIn: true, user: { name: 'Alice' } });
useStatsComposable.useStats.mockReturnValue({
  stats: ref([10, 20, 30]),
  fetchStats: jest.fn()
});

test('Dashboard корректно объединяет composables', () => {
  const wrapper = mount(Dashboard);

  expect(wrapper.vm.isLoggedIn).toBe(true);
  expect(wrapper.vm.user.name).toBe('Alice');
  expect(wrapper.vm.stats).toEqual([10, 20, 30]);
});

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