Моки и стабы

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

Для обеспечения воспроизводимого и изолированного тестирования используются моки (mocks) и стабы (stubs). Эти техники позволяют подменять реальные объекты, функции и зависимости контролируемыми реализациями.

Ключевые задачи моков и стабов:

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

В контексте Interact.js моки и стабы применяются для имитации:

  • объектов interact
  • событий взаимодействия (dragmove, resizemove)
  • DOM-элементов
  • callback-функций
  • координат и параметров движения курсора

Различие между моками и стабами

Несмотря на схожесть назначения, эти инструменты выполняют разные роли.

Стабы

Стаб — это подменённая функция или объект, возвращающий заранее заданный результат.

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

Пример ситуации:

Функция использует координаты события Interact.js:

function calculatePosition(event) {
  return {
    x: event.clientX + 10,
    y: event.clientY + 10
  };
}

Для тестирования можно создать стаб события:

const eventStub = {
  clientX: 100,
  clientY: 200
};

const result = calculatePosition(eventStub);

console.log(result);
// { x: 110, y: 210 }

Стаб заменяет реальный объект события браузера.


Моки

Мок — это объект, который не только подменяет зависимость, но и фиксирует факт взаимодействия с ней.

Моки позволяют проверять:

  • была ли вызвана функция
  • сколько раз она вызвалась
  • с какими аргументами
  • в каком порядке происходили вызовы

Моки особенно полезны при проверке callback-функций Interact.js.

Пример:

function handleDrag(event, updatePosition) {
  updatePosition(event.dx, event.dy);
}

Тестирование:

const mockUpdatePosition = jest.fn();

handleDrag({ dx: 5, dy: 10 }, mockUpdatePosition);

expect(mockUpdatePosition).toHaveBeenCalledWith(5, 10);

Проблемы тестирования Interact.js без моков

Библиотека Interact.js тесно связана с DOM и событиями браузера. Это создаёт несколько сложностей:

1. Необходимость браузерной среды

Interact.js использует:

  • Pointer Events
  • Mouse Events
  • Touch Events

В тестовой среде Node.js такие события отсутствуют.

2. Сложность воспроизведения drag-сценариев

Перетаскивание включает несколько этапов:

  • pointerdown
  • pointermove
  • pointerup

Без моков необходимо вручную эмулировать все события.

3. Сложные зависимости

Функции часто получают:

event.target
event.rect
event.deltaRect
event.dx
event.dy

Создание полноценного объекта события может быть избыточным.

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


Стабирование объектов событий Interact.js

События Interact.js содержат множество полей:

{
  target: HTMLElement,
  dx: number,
  dy: number,
  clientX: number,
  clientY: number,
  rect: {...},
  deltaRect: {...}
}

Для тестирования создаются частичные стабы.

Пример стаба dragmove

const dragEventStub = {
  dx: 15,
  dy: -10,
  target: {
    style: {
      transform: ''
    }
  }
};

Функция обработчика:

function dragMoveListener(event) {
  const x = (parseFloat(event.target.dataset.x) || 0) + event.dx;
  const y = (parseFloat(event.target.dataset.y) || 0) + event.dy;

  event.target.style.transform =
    `translate(${x}px, ${y}px)`;

  event.target.dataset.x = x;
  event.target.dataset.y = y;
}

Тест:

const element = {
  dataset: {},
  style: {}
};

const eventStub = {
  dx: 10,
  dy: 20,
  target: element
};

dragMoveListener(eventStub);

Мокирование функций обратного вызова

Interact.js активно использует callback-функции.

Типичный код:

interact('.item')
  .draggable({
    listeners: {
      move(event) {
        updatePosition(event);
      }
    }
  });

В тестах функция updatePosition может быть замокирована.

Пример с Jest:

const updatePositionMock = jest.fn();

function moveListener(event) {
  updatePositionMock(event.dx, event.dy);
}

moveListener({ dx: 5, dy: 8 });

expect(updatePositionMock).toHaveBeenCalled();

Проверка аргументов:

expect(updatePositionMock)
  .toHaveBeenCalledWith(5, 8);

Стабирование метода interact()

При модульном тестировании иногда требуется подменить саму библиотеку.

Пример кода:

import interact from 'interactjs';

export function initDrag() {
  interact('.box').draggable({
    listeners: {
      move: dragMoveListener
    }
  });
}

В тесте можно стабировать функцию interact.

Пример

jest.mock('interactjs', () => {
  return jest.fn(() => ({
    draggable: jest.fn()
  }));
});

После этого:

import interact from 'interactjs';
import { initDrag } from './drag';

initDrag();

expect(interact).toHaveBeenCalledWith('.box');

Мокирование DOM-элементов

Interact.js работает с DOM-элементами:

event.target.style.transform
event.target.dataset

Для тестов создаётся упрощённый мок DOM-элемента.

Пример

const elementMock = {
  dataset: {
    x: 0,
    y: 0
  },
  style: {
    transform: ''
  }
};

Использование:

const event = {
  dx: 30,
  dy: 40,
  target: elementMock
};

dragMoveListener(event);

Стабирование геометрических параметров resize

События изменения размера содержат сложные структуры.

Пример объекта события:

{
  rect: {
    width: 200,
    height: 100
  },
  deltaRect: {
    left: 5,
    top: 10
  }
}

Стаб события:

const resizeEventStub = {
  rect: {
    width: 300,
    height: 150
  },
  deltaRect: {
    left: 5,
    top: 10
  },
  target: {
    style: {},
    dataset: {}
  }
};

Функция:

function resizeListener(event) {
  event.target.style.width =
    event.rect.width + 'px';

  event.target.style.height =
    event.rect.height + 'px';
}

Тест:

resizeListener(resizeEventStub);

console.log(resizeEventStub.target.style.width);
// 300px

Мокирование цепочек методов Interact.js

API библиотеки построено по принципу fluent interface:

interact('.box')
  .draggable()
  .resizable()
  .gesturable();

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

Пример:

const interactMock = jest.fn(() => ({
  draggable: jest.fn().mockReturnThis(),
  resizable: jest.fn().mockReturnThis(),
  gesturable: jest.fn().mockReturnThis()
}));

Использование:

jest.mock('interactjs', () => interactMock);

Теперь код:

interact('.box')
  .draggable()
  .resizable();

не вызывает реальную библиотеку.


Проверка вызова слушателей событий

Иногда необходимо убедиться, что слушатель события передан корректно.

Исходный код:

interact('.card').draggable({
  listeners: {
    move: dragMoveListener
  }
});

Тест:

const draggableMock = jest.fn();

const interactMock = jest.fn(() => ({
  draggable: draggableMock
}));

jest.mock('interactjs', () => interactMock);

После вызова:

initDrag();

Проверка:

expect(draggableMock).toHaveBeenCalled();

Проверка аргументов:

const options = draggableMock.mock.calls[0][0];

expect(options.listeners.move)
  .toBeDefined();

Стабирование координат курсора

Некоторые функции используют координаты указателя.

Пример:

function getDistance(event) {
  return Math.sqrt(
    event.clientX ** 2 +
    event.clientY ** 2
  );
}

Стаб:

const pointerStub = {
  clientX: 30,
  clientY: 40
};

Тест:

const distance = getDistance(pointerStub);

console.log(distance); // 50

Мокирование сложных сценариев drag

Перетаскивание состоит из последовательности событий:

  1. начало перетаскивания
  2. движение
  3. завершение

Стаб последовательности:

const dragSequence = [
  { dx: 10, dy: 0 },
  { dx: 5, dy: 5 },
  { dx: -3, dy: 2 }
];

Тест:

dragSequence.forEach(event => {
  dragMoveListener({
    ...event,
    target: elementMock
  });
});

Позволяет проверить накопление координат.


Инструменты для работы с моками

Наиболее распространённые библиотеки тестирования:

Jest

Встроенные возможности:

  • jest.fn()
  • jest.mock()
  • mockReturnValue()
  • mockImplementation()

Пример:

const mock = jest.fn();

mock(10);

expect(mock).toHaveBeenCalledTimes(1);

Sinon

Библиотека стабов и моков.

Пример:

const sinon = require('sinon');

const stub = sinon.stub()
  .returns(10);

console.log(stub()); // 10

Testdouble

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


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

Избыточное мокирование

Подмена слишком большого количества зависимостей делает тесты менее надёжными.

Лучше мокировать:

  • внешние библиотеки
  • сетевые запросы
  • DOM-взаимодействия

Тестирование моков вместо логики

Неправильный тест:

expect(mockFn).toHaveBeenCalled();

без проверки результата работы функции.


Слишком сложные стабы

Если стаб повторяет структуру реального объекта полностью, тест становится громоздким.

Лучше использовать минимальный набор свойств.


Стратегии применения моков при тестировании Interact.js

Тестирование чистой логики отдельно

Функции обработки событий должны быть независимыми.

Плохой вариант:

interact('.box').draggable({
  move(event) {
    event.target.style.transform = ...
  }
});

Лучше:

function updateTransform(element, dx, dy) {}

Это упрощает тестирование.


Разделение инициализации и логики

export function setupDrag(selector) {
  interact(selector).draggable({
    listeners: { move: dragMoveListener }
  });
}

Тестируются:

  • логика dragMoveListener
  • вызов interact

Минимальные стабы событий

Для большинства тестов достаточно:

dx
dy
target

Это делает тесты компактными и читаемыми.


Роль моков в архитектуре тестов

Моки и стабы позволяют:

  • тестировать логику взаимодействия без браузера
  • изолировать код обработки событий
  • проверять правильность интеграции с Interact.js
  • воспроизводить сложные пользовательские сценарии

Без использования тестовых двойников тестирование интерфейсов, построенных на событиях перетаскивания и жестов, становится трудоёмким и нестабильным процессом.