При разработке интерфейсов с использованием библиотеки Interact.js часто возникает необходимость тестирования поведения компонентов, зависящих от пользовательских действий: перетаскивания, изменения размеров, жестов и событий взаимодействия. Такие сценарии сложно проверять в изолированных условиях, поскольку библиотека напрямую работает с DOM-элементами и событиями браузера.
Для обеспечения воспроизводимого и изолированного тестирования используются моки (mocks) и стабы (stubs). Эти техники позволяют подменять реальные объекты, функции и зависимости контролируемыми реализациями.
Ключевые задачи моков и стабов:
В контексте Interact.js моки и стабы применяются для имитации:
interactdragmove,
resizemove)Несмотря на схожесть назначения, эти инструменты выполняют разные роли.
Стаб — это подменённая функция или объект, возвращающий заранее заданный результат.
Основная цель стабов — контроль поведения зависимостей.
Пример ситуации:
Функция использует координаты события 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 тесно связана с DOM и событиями браузера. Это создаёт несколько сложностей:
1. Необходимость браузерной среды
Interact.js использует:
В тестовой среде Node.js такие события отсутствуют.
2. Сложность воспроизведения drag-сценариев
Перетаскивание включает несколько этапов:
Без моков необходимо вручную эмулировать все события.
3. Сложные зависимости
Функции часто получают:
event.target
event.rect
event.deltaRect
event.dx
event.dy
Создание полноценного объекта события может быть избыточным.
Моки и стабы позволяют сосредоточиться только на нужных свойствах.
События Interact.js содержат множество полей:
{
target: HTMLElement,
dx: number,
dy: number,
clientX: number,
clientY: number,
rect: {...},
deltaRect: {...}
}
Для тестирования создаются частичные стабы.
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);
При модульном тестировании иногда требуется подменить саму библиотеку.
Пример кода:
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');
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);
События изменения размера содержат сложные структуры.
Пример объекта события:
{
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
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
Перетаскивание состоит из последовательности событий:
Стаб последовательности:
const dragSequence = [
{ dx: 10, dy: 0 },
{ dx: 5, dy: 5 },
{ dx: -3, dy: 2 }
];
Тест:
dragSequence.forEach(event => {
dragMoveListener({
...event,
target: elementMock
});
});
Позволяет проверить накопление координат.
Наиболее распространённые библиотеки тестирования:
Встроенные возможности:
jest.fn()jest.mock()mockReturnValue()mockImplementation()Пример:
const mock = jest.fn();
mock(10);
expect(mock).toHaveBeenCalledTimes(1);
Библиотека стабов и моков.
Пример:
const sinon = require('sinon');
const stub = sinon.stub()
.returns(10);
console.log(stub()); // 10
Минималистичная библиотека для создания тестовых двойников.
Подмена слишком большого количества зависимостей делает тесты менее надёжными.
Лучше мокировать:
Неправильный тест:
expect(mockFn).toHaveBeenCalled();
без проверки результата работы функции.
Если стаб повторяет структуру реального объекта полностью, тест становится громоздким.
Лучше использовать минимальный набор свойств.
Функции обработки событий должны быть независимыми.
Плохой вариант:
interact('.box').draggable({
move(event) {
event.target.style.transform = ...
}
});
Лучше:
function updateTransform(element, dx, dy) {}
Это упрощает тестирование.
export function setupDrag(selector) {
interact(selector).draggable({
listeners: { move: dragMoveListener }
});
}
Тестируются:
dragMoveListenerinteractДля большинства тестов достаточно:
dx
dy
target
Это делает тесты компактными и читаемыми.
Моки и стабы позволяют:
Без использования тестовых двойников тестирование интерфейсов, построенных на событиях перетаскивания и жестов, становится трудоёмким и нестабильным процессом.