При работе с библиотекой Cleave.js часто возникает необходимость тестировать код, связанный с форматированием пользовательского ввода, не используя полноценный браузерный интерфейс. В таких сценариях применяются моки (mocks), стабы (stubs) и фейковые объекты.
Основные причины использования моков:
Cleave.js активно взаимодействует с:
Из-за этого библиотека требует специального подхода к тестированию.
Stub подменяет поведение зависимости фиксированным результатом.
Пример:
const getRawValueStub = () => '4111111111111111';
Stub обычно не проверяет количество вызовов и аргументы.
Mock не только имитирует поведение, но и отслеживает взаимодействия.
Пример с Jest:
const onValueChangedM ock = jest.fn();
Mock позволяет проверять:
Fake представляет собой упрощённую реализацию настоящего объекта.
Пример fake input:
const fakeInput = {
value: '',
addEventListener() {},
removeEventListener() {}
};
Библиотека:
Обычный unit-тест без моков часто приводит к ошибкам:
TypeError: Cannot read property 'selectionEnd' of undefined
или:
document is not defined
Для многих тестов достаточно базовой структуры:
const inputMock = {
value: '',
selectionStart: 0,
selectionEnd: 0,
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
setSelectionRange: jest.fn()
};
Некоторые свойства input являются readonly.
Для их мокирования применяется:
Object.defineProperty(inputMock, 'rawValue', {
value: '123456',
writable: true
});
Одной из наиболее важных функций Cleave.js является callback
onValueChanged.
Пример:
const onValueChan ged = jest.fn();
const cleave = new Cleave(input, {
creditCard: true,
onValueChanged
});
Проверка:
expect(onValueChanged).toHaveBeenCalled();
expect(onValueChanged).toHaveBeenCalledWith(
expect.objectContaining({
target: expect.objectContaining({
value: '4111 1111 1111 1111'
})
})
);
Иногда необходимо изолировать код от реального форматирования.
Пример:
jest.spyOn(cleave, 'getRawValue')
.mockReturnValue('4111111111111111');
Теперь метод всегда возвращает фиксированное значение.
jest.spyOn(cleave, 'destroy')
.mockImplementation(() => {});
Подобный подход полезен при тестировании cleanup-логики.
Часто используется связка:
npm install jest jsdom --save-dev
Конфигурация:
module.exports = {
testEnvironment: 'jsdom'
};
jsdom обеспечивает имитацию браузерного окружения.
document.body.innerHTML = `
<input id="card" />
`;
const input = document.getElementById('card');
После этого Cleave.js может работать почти как в браузере.
const event = new Event('input');
input.value = '4111111111111111';
input.dispatchEvent(event);
const keydown = new KeyboardEvent('keydown', {
key: '1'
});
input.dispatchEvent(keydown);
Cleave.js активно работает с:
Без их мокирования тесты становятся нестабильными.
Object.defineProperty(input, 'selectionStart', {
writable: true,
value: 0
});
Object.defineProperty(input, 'selectionEnd', {
writable: true,
value: 0
});
input.setSelectionRange = jest.fn();
При тестировании бизнес-логики форматирование карты может быть несущественным.
Тогда formatter стабируется:
jest.spyOn(Cleave.DefaultProperties, 'creditCard')
.mockImplementation(() => {});
Некоторые сценарии используют debounce или асинхронные операции.
Jest позволяет стабировать таймеры:
jest.useFakeTimers();
setTimeout(callback, 1000);
jest.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalled();
const pasteEvent = new Event('paste');
pasteEvent.clipboardData = {
getData: () => '4111111111111111'
};
input.dispatchEvent(pasteEvent);
Некоторые плагины и окружения используют navigator.
Пример:
Object.defineProperty(global, 'navigator', {
value: {
userAgent: 'jest'
},
configurable: true
});
global.window = Object.create(window);
Object.defineProperty(window, 'innerWidth', {
value: 1920
});
В React-проектах часто стабируют сам компонент Cleave.
Пример:
jest.mock('cleave.js/react', () => {
return function MockCleave(props) {
return (
<input
data-testid="cleave"
onCha nge={props.onChange}
/>
);
};
});
fireEvent.change(input, {
target: {
value: '4111111111111111'
}
});
expect(input.value)
.toBe('4111 1111 1111 1111');
global.stubs = {
Cleave: {
template: '<input />'
}
};
class FakeCleaveDirective {
writeValue() {}
registerOnChange() {}
}
const spy = jest.spyOn(cleave, 'setRawValue');
cleave.setRawValue('1234');
expect(spy).toHaveBeenCalledWith('1234');
Ошибка:
Cannot read property of undefined
Причина:
const method = cleave.getRawValue;
method();
Контекст теряется.
const method = cleave.getRawValue.bind(cleave);
jsdom не всегда содержит requestAnimationFrame.
Исправление:
global.requestAnimationFrame = callback => {
return setTimeout(callback, 0);
};
const formatterMock = jest.fn(value => {
return value.toUpperCase();
});
formatterMock
.mockReturnValueOnce('1111')
.mockReturnValueOnce('2222');
jest.mock('cleave.js', () => {
return jest.fn().mockImplementation(() => {
return {
destroy: jest.fn(),
getRawValue: jest.fn(() => '1234')
};
});
});
jest.mock('cleave.js', () => {
const original = jest.requireActual('cleave.js');
return {
...original,
formatCreditCard: jest.fn()
};
});
jest.spyOn(cleave, 'setRawValue')
.mockImplementation(() => {
throw new Error('Formatting failed');
});
Проверка:
expect(() => {
cleave.setRawValue('123');
}).toThrow('Formatting failed');
const mockApi = {
setRawValue: jest.fn().mockReturnThis(),
destroy: jest.fn().mockReturnThis()
};
afterEach(() => {
jest.clearAllMocks();
});
afterEach(() => {
jest.resetAllMocks();
});
Очищает:
Но сохраняет implementation.
Полностью сбрасывает:
Если один экземпляр Cleave.js используется между тестами:
const cleave = new Cleave(...);
состояние может утекать между test cases.
beforeEach(() => {
input = document.createElement('input');
cleave = new Cleave(input, {
numeral: true
});
});
jest.spyOn(document, 'createElement')
.mockImplementation(tag => {
return {
tagName: tag.toUpperCase(),
value: ''
};
});
expect(input.removeEventListener)
.toHaveBeenCalled();
const removeSpy = jest.spyOn(
input,
'removeEventListener'
);
cleave.destroy();
expect(removeSpy).toHaveBeenCalled();
global.navigator.clipboard = {
writeText: jest.fn(),
readText: jest.fn()
};
expect(input.outerHTML)
.toMatchSnapshot();
expect(cleave.getRawValue())
.toBe('4111111111111111');
expect(input.value)
.toBe('4111 1111 1111 1111');
Чрезмерное количество mock-объектов может скрывать реальные ошибки:
Обычно применяется схема:
| Тип теста | Использование моков |
|---|---|
| Unit | Максимальное |
| Integration | Частичное |
| E2E | Минимальное |
| Browser testing | Отсутствует |
Удобно выносить mocks в отдельный модуль:
export function createInputMock() {
return {
value: '',
selectionStart: 0,
selectionEnd: 0,
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
setSelectionRange: jest.fn()
};
}
export function triggerInput(element, value) {
element.value = value;
element.dispatchEvent(
new Event('input')
);
}
const customFormatter = jest.fn(value => {
return value.replace(/\D/g, '');
});
expect(customFormatter)
.toHaveBeenCalledTimes(1);
expect(mock1.mock.invocationCallOrder[0])
.toBeLessThan(
mock2.mock.invocationCallOrder[0]
);
await Promise.resolve();
expect(callback).toHaveBeenCalled();
Если форматирование связано с серверной логикой:
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({
valid: true
})
})
);
Хорошая структура тестирования Cleave.js обычно включает:
Проблемный подход:
global.document = {};
Большинство внутренних механизмов Cleave.js перестанет работать.
Избыточное количество spyOn:
jest.spyOn(cleave, 'method1');
jest.spyOn(cleave, 'method2');
jest.spyOn(cleave, 'method3');
делает тесты хрупкими и зависимыми от внутренней реализации.
import Cleave from 'cleave.js';
describe('Credit card formatter', () => {
let input;
let cleave;
beforeEach(() => {
document.body.innerHTML = `
<input id="card" />
`;
input = document.getElementById('card');
cleave = new Cleave(input, {
creditCard: true
});
});
afterEach(() => {
cleave.destroy();
jest.clearAllMocks();
});
test('formats card correctly', () => {
input.value = '4111111111111111';
input.dispatchEvent(
new Event('input')
);
expect(input.value)
.toBe('4111 1111 1111 1111');
expect(cleave.getRawValue())
.toBe('4111111111111111');
});
});