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

Мокирование зависимостей — это изоляция компонентов и модулей от их реальных внешних зависимостей с целью тестирования логики в контролируемых условиях. В контексте Riot.js это особенно важно, так как компоненты тесно связаны с состоянием, событиями, HTTP-запросами и сторонними сервисами. Без моков тесты становятся нестабильными, медленными и зависимыми от внешней среды.

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


Типичные зависимости в компонентах Riot.js

В реальных приложениях Riot-компоненты редко существуют изолированно. Наиболее распространённые зависимости:

  • HTTP-клиенты (fetch, axios)
  • Глобальное состояние (store, event bus)
  • Таймеры и асинхронные операции
  • Внешние утилиты и сервисы
  • DOM API и браузерные объекты

Каждый из этих типов требует собственного подхода к мокированию.


Мокирование функций и сервисов

Самый простой и часто используемый вариант — подмена функций. В Riot.js логика часто выносится в отдельные модули, которые импортируются в компонент.

// api.js
export function loadUsers() {
  return fetch('/api/users').then(r => r.json())
}

В компоненте:

import { loadUsers } FROM './api'

export default {
  async onMounted() {
    this.users = await loadUsers()
  }
}

Для тестирования такой компонент должен получать управляемую версию loadUsers. Это достигается через мокирование модуля на уровне тестовой среды.

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

jest.mock('./api', () => ({
  loadUsers: jest.fn()
}))

После этого поведение зависимости задаётся явно:

import { loadUsers } from './api'

loadUsers.mockResolvedValue([
  { id: 1, name: 'Alice' }
])

Компонент работает с подставными данными, не выполняя реальных HTTP-запросов.


Мокирование асинхронных операций

Асинхронность в Riot.js встречается повсеместно: загрузка данных, debounce, таймеры. Для управления временем используются поддельные таймеры.

jest.useFakeTimers()

Пример логики в компоненте:

this.timeoutId = setTimeout(() => {
  this.visible = true
}, 1000)

В тесте:

jest.advanceTimersByTime(1000)

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


Мокирование глобального состояния и event bus

Riot.js не навязывает конкретный state manager, поэтому часто используется собственная реализация или простой event bus.

// bus.js
import mitt from 'mitt'
export const bus = mitt()

Компоненты подписываются на события:

bus.on('login', user => {
  this.user = user
})

В тестах реальный bus заменяется на мок:

jest.mock('./bus', () => ({
  bus: {
    on: jest.fn(),
    emit: jest.fn()
  }
}))

Это позволяет:

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

Мокирование DOM и браузерных API

Компоненты Riot.js взаимодействуют с DOM напрямую или через this.root. В тестовой среде (например, jsdom) доступны базовые DOM API, но некоторые возможности требуют мокирования:

  • localStorage
  • IntersectionObserver
  • ResizeObserver
  • matchMedia

Пример мокирования localStorage:

Object.defineProperty(window, 'localStorage', {
  value: {
    getItem: jest.fn(),
    setItem: jest.fn()
  }
})

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


Инъекция зависимостей как альтернатива мокам

Жёсткие импорты усложняют тестирование. Более гибкий подход — передача зависимостей через параметры или опции компонента.

export default function createComponent(api) {
  return {
    async onMounted() {
      this.data = await api.load()
    }
  }
}

В тесте:

const apiMock = {
  load: jest.fn().mockResolvedValue([1, 2, 3])
}

const component = createComponent(apiMock)

Такой подход:

  • Упрощает тестирование
  • Снижает связанность
  • Делает архитектуру предсказуемой

Мокирование слотов и дочерних компонентов

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

const ChildStub = {
  template: '<div></div>'
}

Это позволяет сосредоточиться на логике родительского компонента, не затрагивая внутреннее устройство дочерних.


Проверка взаимодействия с моками

Мокирование бесполезно без проверки факта использования зависимости. Основные проверки:

  • Количество вызовов
  • Переданные аргументы
  • Порядок вызовов

Пример:

expect(loadUsers).toHaveBeenCalledTimes(1)
expect(loadUsers).toHaveBeenCalledWith({ LIMIT: 10 })

Такие проверки подтверждают корректность интеграции компонента с внешней логикой.


Распространённые ошибки при мокировании

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

Мокирование внутренней логики Не следует мокировать функции, которые являются частью тестируемого поведения.

Зависимость тестов от реализации Тесты не должны знать, как именно компонент реализует логику, а только что он делает.


Практический эффект мокирования в Riot.js

Грамотно выстроенное мокирование:

  • Делает тесты быстрыми и стабильными
  • Позволяет тестировать сложные сценарии
  • Упрощает рефакторинг компонентов
  • Повышает качество архитектуры

В экосистеме Riot.js, где разработчик сам определяет структуру приложения, мокирование становится не просто техникой тестирования, а инструментом проектирования и контроля сложности.