Моки и стабы для внешних зависимостей

Работа с внешними зависимостями в JavaScript-приложениях часто требует изоляции компонентов для тестирования. Stimulus, как фреймворк для управления поведением пользовательского интерфейса, позволяет легко подключать внешние сервисы, однако для обеспечения надёжных тестов необходимо использовать моки и стабы.


Основные понятия

Мок (Mock) — это объект, который имитирует поведение реального компонента или сервиса, но при этом позволяет отслеживать вызовы методов и проверять взаимодействие. Моки полезны для тестирования интеграций, когда важны именно вызовы методов и передаваемые данные.

Стаб (Stub) — это объект, предоставляющий заранее определённые ответы на вызовы функций, но без контроля за частотой или параметрами вызовов. Стабы упрощают тестирование компонентов, которые зависят от внешних данных, но не требуют проверки взаимодействия.


Использование моков и стабов в Stimulus

Stimulus строится вокруг контроллеров, каждый из которых управляет поведением определённого элемента DOM. Контроллеры часто взаимодействуют с внешними API, сервисами или библиотеками. Чтобы тестировать контроллеры в изоляции, внешние зависимости заменяются моками или стабами.

Пример стандартной структуры контроллера:

// app/javascript/controllers/notification_controller.js
import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static values = { endpoint: String };

  connect() {
    this.loadNotifications();
  }

  async loadNotifications() {
    const response = await fetch(this.endpointValue);
    const data = await response.json();
    this.renderNotifications(data);
  }

  renderNotifications(notifications) {
    this.element.innerHTML = notifications.map(n => `<p>${n.message}</p>`).join('');
  }
}

В этом примере fetch является внешней зависимостью. Для тестирования контроллера без реального запроса к серверу можно использовать мок или стаб.


Создание стаба для fetch

Стаб возвращает заранее определённый результат:

global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve([{ message: "Test notification" }]),
  })
);

Теперь вызов loadNotifications() будет получать фиктивные данные без обращения к сети. Такой подход позволяет проверять только логику рендеринга и обработку данных, не полагаясь на реальный сервер.


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

Моки позволяют не только подменять функционал, но и контролировать вызовы методов:

const mockFetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve([{ message: "Mocked notification" }]),
  })
);

global.fetch = mockFetch;

// В тесте можно проверить, что fetch вызван с правильным URL
await controller.loadNotifications();
expect(mockFetch).toHaveBeenCalledWith(controller.endpointValue);

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


Моки и стабы для других внешних зависимостей

Stimulus-контроллеры часто используют:

  • Локальное хранилище (localStorage / sessionStorage) Для тестирования можно создать простые объекты с методами getItem, setItem и removeItem, которые имитируют стандартное поведение.

  • События и кастомные события Мок-объект для события может проверять, был ли вызван метод dispatchEvent и с какими данными.

  • Сторонние библиотеки UI или анимации Вместо подключения реальной библиотеки используется стаб, который не выполняет анимации, но возвращает ожидаемые данные.


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

Jest предоставляет удобные возможности для создания моков и стабов:

  • jest.fn() — создание функции-заглушки, отслеживающей вызовы.
  • jest.mock('module') — автоматическая подмена модуля мок-версией.
  • jest.spyOn(object, 'method') — создание шпиона, позволяющего контролировать и подменять вызовы метода.

Пример мокирования внешнего сервиса в Stimulus:

import NotificationController from "./notification_controller";

jest.mock("./notification_service", () => ({
  fetchNotifications: jest.fn(() => Promise.resolve([{ message: "Mocked" }])),
}));

test("контроллер рендерит уведомления", async () => {
  document.body.innerHTML = `<div data-controller="notification" data-notification-endpoint-value="/api/notifications"></div>`;
  const element = document.querySelector("[data-controller='notification']");
  const controller = new NotificationController(element);
  await controller.loadNotifications();

  expect(element.innerHTML).toContain("Mocked");
});

Рекомендации по применению

  1. Изоляция тестируемого кода — каждый контроллер тестируется отдельно от реальных API.
  2. Подмена только внешних зависимостей — внутренняя логика остаётся нетронутой.
  3. Явное управление поведением моков — заранее определять результаты и контролировать вызовы.
  4. Использование стаба для статических данных — если результат известен и не требуется проверка взаимодействий.
  5. Комбинация моков и стабов — для сложных сценариев удобно использовать стабы для данных и моки для проверки вызовов сервисов.

Моки и стабы обеспечивают надёжное тестирование Stimulus-контроллеров, повышают стабильность тестов и позволяют разрабатывать UI независимо от состояния серверов и сторонних библиотек. Этот подход критически важен для поддержания качественной архитектуры фронтенд-приложений и ускоряет процесс разработки.