Моки и стабы

При тестировании компонентов, зависящих от внешних модулей и побочных эффектов, ключевой задачей становится контроль окружения. Библиотека выбора значений в интерфейсе активно взаимодействует с DOM, событиями, таймерами, сетевыми запросами и состоянием страницы. В таких условиях прямое тестирование приводит к нестабильным результатам: тесты становятся чувствительными к реализации зависимостей, времени выполнения и асинхронным операциям.

Для устранения этих проблем используются моки и стабы — техники подмены зависимостей управляемыми заглушками, позволяющими полностью контролировать поведение внешних компонентов.

Разделение понятий: mock, stub, spy

В экосистеме тестирования часто смешиваются три близких концепции, хотя их назначение различается.

Stub (стаб) Используется для замены зависимости с заранее заданным поведением. Основная цель — вернуть фиксированные данные без выполнения реальной логики.

const apiStub = {
  search: async (query) => {
    return [
      { id: 1, text: "Alpha" },
      { id: 2, text: "Beta" }
    ];
  }
};

Стаб не содержит логики проверки вызовов, он лишь подменяет результат.

Mock (мок) Расширяет стаб, добавляя возможность проверять взаимодействие: сколько раз вызван метод, с какими аргументами, в каком порядке.

const searchMock = jest.fn().mockResolvedValue([
  { id: 1, text: "Alpha" }
]);

Spy (шпион) Оборачивает реальную функцию и фиксирует её вызовы без полной подмены поведения.

const spy = jest.spyOn(console, "log");

Почему Tom Select требует изоляции в тестах

Компонент управления выпадающими списками в JavaScript активно зависит от:

  • DOM-структуры
  • событий ввода пользователя
  • асинхронных источников данных
  • рендера и перерасчёта элементов
  • внутреннего состояния выбора

При тестировании напрямую возникают проблемы:

  • нестабильный DOM в JSDOM
  • асинхронные гонки при загрузке данных
  • невозможность контролировать внешние API
  • сложность воспроизведения пользовательских действий

Поэтому ключевая стратегия — подмена внешних зависимостей через моки и стабы.

Моки DOM-окружения

Во многих тестах требуется изолировать работу с DOM. Например, создание контейнера:

beforeEach(() => {
  document.body.innerHTML = `<select id="select"></select>`;
});

Однако более сложные случаи требуют мокирования методов DOM API.

Подмена querySelector

const querySelectorMock = jest.fn((selector) => {
  if (selector === "#select") {
    return document.createElement("select");
  }
  return null;
});

document.querySelector = querySelectorMock;

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

Стабилизация асинхронных источников данных

Tom Select часто используется с удалёнными данными через load или кастомные функции поиска. В тестах важно исключить реальные запросы.

Стаб загрузки данных

const loadStub = (query, callback) => {
  const data = [
    { value: "1", text: "Москва" },
    { value: "2", text: "Минск" }
  ];
  callback(data);
};

Этот стаб заменяет реальный AJAX или fetch и обеспечивает детерминированный результат.

Мок fetch

global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () =>
      Promise.resolve([
        { id: 1, name: "Item A" },
        { id: 2, name: "Item B" }
      ])
  })
);

Такой мок позволяет полностью контролировать асинхронный поток данных.

Подмена внутренних методов Tom Select

При интеграционном тестировании важно изолировать поведение отдельных методов библиотеки.

Мок метода addItem

const instance = new TomSelect("#select");

instance.addItem = jest.fn();
instance.addItem("value1");

expect(instance.addItem).toHaveBeenCalledWith("value1");

Здесь проверяется не результат работы метода, а факт вызова с корректными аргументами.

Использование jest.fn для событий

Tom Select активно генерирует события: change, dropdown_open, item_add. Их удобно имитировать через мок-функции.

const onChangeM ock = jest.fn();

const select = new TomSelect("#select", {
  onChange: onChangeMock
});

select.setValue("123");

expect(onChangeMock).toHaveBeenCalled();

Такой подход позволяет тестировать реакцию системы без реального взаимодействия с UI.

Моки таймеров и задержек

Некоторые сценарии зависят от задержек, например debounce при поиске.

Подмена таймеров

jest.useFakeTimers();

const select = new TomSelect("#select", {
  loadThrottle: 300
});

select.load("test");

jest.advanceTimersByTime(300);

Фальшивые таймеры устраняют необходимость реального ожидания и делают тесты детерминированными.

Стабирование событий DOM

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

const selectEl = document.getElementById("select");

const event = new Event("change");
selectEl.dispatchEvent(event);

Для более сложных сценариев создаются стабы обработчиков событий:

const eventHandlerStub = jest.fn();

selectEl.addEventListener("change", eventHandlerStub);

selectEl.dispatchEvent(new Event("change"));

expect(eventHandlerStub).toHaveBeenCalled();

Подмена конфигурации Tom Select

Конфигурационные параметры часто влияют на поведение компонентов. Для тестирования используется фиктивная конфигурация.

const configStub = {
  maxItems: 3,
  create: false,
  placeholder: "Выбор"
};

const select = new TomSelect("#select", configStub);

При необходимости параметры также подменяются динамически:

select.settings.maxItems = 1;

Изоляция плагинов через моки

Tom Select поддерживает плагины, которые могут изменять поведение.

Для тестирования используется отключение или подмена:

const select = new TomSelect("#select", {
  plugins: []
});

Или мокирование плагина:

jest.mock("tom-select/plugins/drag_drop", () => {
  return function () {
    return {
      init: jest.fn(),
      destroy: jest.fn()
    };
  };
});

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

Основная ценность моков проявляется в проверке взаимодействий, а не результата.

const removeItemMock = jest.fn();

const select = new TomSelect("#select");
select.removeItem = removeItemMock;

select.removeItem("value1");

expect(removeItemMock).toHaveBeenCalledTimes(1);
expect(removeItemMock).toHaveBeenCalledWith("value1");

Такой подход позволяет тестировать контракт поведения, а не внутреннюю реализацию.

Комбинирование стаба и мока

На практике часто используется гибридный подход: стаб возвращает данные, мок фиксирует вызовы.

const loadMock = jest.fn((query, cb) => {
  cb([{ value: "1", text: "A" }]);
});

const select = new TomSelect("#select", {
  load: loadMock
});

select.load("test");

expect(loadMock).toHaveBeenCalledWith("test", expect.any(Function));

Контроль состояния через моки

Tom Select хранит внутреннее состояние выбранных значений. В тестах его можно фиксировать вручную.

const select = new TomSelect("#select");

select.setValue("1");
select.setValue = jest.fn();

select.setValue("2");

expect(select.setValue).toHaveBeenCalled();

Типичные ошибки при использовании моков

Чрезмерное использование моков приводит к тестам, которые проверяют не поведение системы, а сам факт вызовов. Это снижает ценность тестирования.

Проблемные паттерны:

  • мокирование всего объекта Tom Select целиком
  • отсутствие проверки состояния DOM
  • игнорирование асинхронных цепочек
  • подмена логики вместо изоляции зависимости

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

Контроль побочных эффектов

Особое внимание требуется побочным эффектам: изменению DOM, добавлению классов, созданию элементов.

const select = new TomSelect("#select");

select.open();

expect(document.querySelector(".ts-dropdown")).not.toBeNull();

Если DOM нестабилен, используется частичная подмена:

document.createElement = jest.fn(() => {
  return {
    classList: {
      add: jest.fn()
    }
  };
});

Итоговая модель изоляции

В тестировании компонентов уровня Tom Select формируется типовая схема:

  • стабы — для данных
  • моки — для проверки вызовов
  • спаи — для наблюдения за реальными функциями
  • фейковые таймеры — для асинхронности
  • DOM-заглушки — для окружения

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