Sinon для JavaScript (Node.js): полное руководство с примерами
1. Что это и зачем нужна
Sinon.js — это мощная библиотека для JavaScript, которая предоставляет автономные тестовые дублёры: шпионы (spies), стабы (stubs) и моки (mocks). Она не зависит от конкретного тестового фреймворка (Mocha, Jest, Jasmine и др.) и может использоваться с любым из них. Основная цель Sinon — изолировать тестируемый код от внешних зависимостей (базы данных, API, файловой системы, таймеров), позволяя проверять поведение функций в контролируемом окружении.
Зачем это нужно? В реальных приложениях функции часто вызывают другие функции, обращаются к внешним сервисам или работают с асинхронными операциями. Без Sinon вы не сможете легко проверить, была ли вызвана функция обратного вызова, с какими аргументами, сколько раз, или подменить ответ от сервера. Sinon решает эти проблемы элегантно и лаконично, делая тесты быстрыми, надёжными и предсказуемыми.
Библиотека особенно популярна в экосистеме Node.js, где модульное тестирование критически важно. Она позволяет тестировать функции, не запуская реальные HTTP-запросы, не обращаясь к базе данных и не дожидаясь таймеров. Это ускоряет выполнение тестов в десятки раз и делает их детерминированными.
2. Установка
Sinon устанавливается через npm. Для Node.js выполните команду:
npm install sinon --save-dev
Если вы используете TypeScript, установите также типы:
npm install sinon @types/sinon --save-dev
Для использования в браузере можно подключить CDN-версию:
<script src="https://cdn.jsdelivr.net/npm/sinon@latest/pkg/sinon.js"></script>
После установки импортируйте библиотеку в вашем тестовом файле:
const sinon = require('sinon'); // CommonJS
// или
import sinon from 'sinon'; // ES Modules
3. Быстрый старт — минимальный рабочий пример
Рассмотрим простейший пример: функция, которая вызывает колбэк с приветствием. Мы хотим проверить, что колбэк был вызван с правильным аргументом.
const sinon = require('sinon');
// Функция, которую мы тестируем
function greet(name, callback) {
callback(`Hello, ${name}!`);
}
// Тест с использованием шпиона (spy)
const callbackSpy = sinon.spy();
greet('Alice', callbackSpy);
console.log(callbackSpy.calledOnce); // true
console.log(callbackSpy.calledWith('Hello, Alice!')); // true
Этот пример демонстрирует базовую идею: мы создаём шпион, передаём его в тестируемую функцию, а затем проверяем, как он был вызван. В реальных тестах вы будете использовать утверждения (assertions) из вашего тестового фреймворка.
4. Основные методы/функции/классы
4.1. sinon.spy() — Шпион
Сигнатура: sinon.spy([object], [method]) или sinon.spy([func])
Что делает: Создаёт шпион, который записывает информацию о вызовах функции: количество вызовов, аргументы, контекст, возвращаемое значение. Может оборачивать существующую функцию или заменять метод объекта.
Пример:
const user = {
setName: function(name) { this.name = name; }
};
const spy = sinon.spy(user, 'setName');
user.setName('John');
console.log(spy.calledOnce); // true
console.log(spy.calledWith('John')); // true
console.log(spy.firstCall.args); // ['John']
spy.restore(); // обязательно восстановить оригинальный метод
4.2. sinon.stub() — Стаб (заглушка)
Сигнатура: sinon.stub([object], [method])
Что делает: Создаёт заглушку, которая заменяет функцию и позволяет предопределить её поведение: возвращать конкретное значение, выбрасывать исключение, вызывать колбэк с определёнными аргументами. Стаб также записывает информацию о вызовах, как шпион.
Пример:
const db = {
findUser: function(id) { /* реальный запрос к БД */ }
};
const stub = sinon.stub(db, 'findUser');
stub.withArgs(1).returns({ id: 1, name: 'Alice' });
stub.withArgs(2).throws(new Error('User not found'));
console.log(db.findUser(1)); // { id: 1, name: 'Alice' }
console.log(db.findUser(2)); // Error: User not found
stub.restore();
4.3. sinon.mock() — Мок
Сигнатура: sinon.mock(object)
Что делает: Создаёт мок для объекта. Мок — это более строгий дублёр: он не только заменяет методы, но и задаёт ожидания (expectations) — какие методы должны быть вызваны, с какими аргументами, сколько раз. Если ожидания не совпадают, мок выбрасывает ошибку при проверке.
Пример:
const api = {
fetchData: function(url) { /* реальный запрос */ }
};
const mock = sinon.mock(api);
mock.expects('fetchData')
.once()
.withArgs('/users')
.returns([{ id: 1 }]);
// Тестируемый код
api.fetchData('/users');
mock.verify(); // Проверка: если ожидания не выполнены — ошибка
mock.restore();
4.4. sinon.fake() — Фейк (замена)
Сигнатура: sinon.fake([func]) или sinon.fake.returns(value) / sinon.fake.throws(error)
Что делает: Создаёт простую фейковую функцию, которая может возвращать значение, выбрасывать исключение или выполнять пользовательскую логику. В отличие от stub, fake не имеет методов для проверки вызовов (но их можно добавить через sinon.spy(fake)).
Пример:
const fakeSuccess = sinon.fake.returns('OK');
console.log(fakeSuccess()); // 'OK'
const fakeError = sinon.fake.throws(new Error('Fail'));
// fakeError(); // Error: Fail
// Комбинируем со шпионом
const spyOnFake = sinon.spy(sinon.fake.returns(42));
console.log(spyOnFake()); // 42
console.log(spyOnFake.calledOnce); // true
4.5. sinon.useFakeTimers() — Фейковые таймеры
Сигнатура: sinon.useFakeTimers([now], [config])
Что делает: Заменяет глобальные функции работы со временем (setTimeout, setInterval, Date.now, performance.now и др.) на фейковые, которые можно контролировать вручную. Позволяет "прокручивать" время в тестах без реальных задержек.
Пример:
const clock = sinon.useFakeTimers();
let callbackCalled = false;
setTimeout(() => { callbackCalled = true; }, 1000);
console.log(callbackCalled); // false
clock.tick(1000); // прокручиваем время на 1 секунду
console.log(callbackCalled); // true
clock.restore();
4.6. sinon.fakeServer — Фейковый сервер (XHR)
Сигнатура: sinon.fakeServer.create([config])
Что делает: Создаёт фейковый сервер для перехвата XMLHttpRequest-запросов в браузере. Позволяет задавать предопределённые ответы на запросы без реального HTTP-вызова.
Пример:
const server = sinon.fakeServer.create();
server.respondWith('GET', '/api/users', [
200,
{ 'Content-Type': 'application/json' },
JSON.stringify([{ id: 1, name: 'Alice' }])
]);
// Где-то в коде делается XHR-запрос
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/users');
xhr.onload = function() {
console.log(JSON.parse(this.responseText)); // [{ id: 1, name: 'Alice' }]
};
xhr.send();
server.respond(); // заставить сервер ответить
server.restore();
4.7. sinon.match() — Сопоставление аргументов
Сигнатура: sinon.match(value) или sinon.match(type), sinon.match.string, sinon.match.number и др.
Что делает: Предоставляет гибкие сопоставители для проверки аргументов. Позволяет проверять не точное равенство, а соответствие типу, регулярному выражению, объекту с определёнными полями и т.д.
Пример:
const stub = sinon.stub();
stub(sinon.match.string, sinon.match.number);
stub(sinon.match({ id: sinon.match.number, name: sinon.match.string }));
console.log(stub.calledWith('hello', 42)); // true
console.log(stub.calledWith({ id: 1, name: 'Alice' })); // true
// Другие полезные сопоставители:
sinon.match.any // любое значение
sinon.match.defined // не undefined
sinon.match.array // массив
sinon.match.regexp(/^test/) // строка, подходящая под регулярку
4.8. sinon.assert — Утверждения
Сигнатура: sinon.assert.method(spy, [arg1, arg2, ...])
Что делает: Набор вспомогательных функций для проверки состояния шпионов, стабов и моков. Если проверка не проходит, выбрасывается исключение с понятным сообщением.
Пример:
const spy = sinon.spy();
spy('arg1', 'arg2');
sinon.assert.calledOnce(spy);
sinon.assert.calledWith(spy, 'arg1', 'arg2');
sinon.assert.calledWithExactly(spy, 'arg1', 'arg2');
// Другие полезные утверждения:
sinon.assert.notCalled(spy);
sinon.assert.callOrder(spy1, spy2);
sinon.assert.alwaysCalledWith(spy, 'arg');
4.9. sinon.createStubInstance() — Стаб для класса
Сигнатура: sinon.createStubInstance(Constructor)
Что делает: Создаёт экземпляр класса, где все методы заменены стабами. Полезно для тестирования классов-зависимостей без вызова реального конструктора.
Пример:
class Database {
constructor() { /* сложная инициализация */ }
query(sql) { /* реальный запрос */ }
}
const dbStub = sinon.createStubInstance(Database);
dbStub.query.returns([{ id: 1 }]);
console.log(dbStub.query('SELECT * FROM users')); // [{ id: 1 }]
console.log(dbStub.query.calledOnce); // true
4.10. sinon.replace() и sinon.replaceGetter()/sinon.replaceSetter()
Сигнатура: sinon.replace(object, property, replacement)
Что делает: Заменяет свойство объекта (функцию, геттер, сеттер) на указанное значение. Отличается от stub тем, что может заменить не только функции, но и любые значения.
Пример:
const config = { timeout: 5000 };
sinon.replace(config, 'timeout', 100);
console.log(config.timeout); // 100
sinon.restore(); // восстанавливает все замены
5. Реальный практический пример
Представим, что у нас есть сервис для отправки email-уведомлений и функция регистрации пользователя. Мы хотим протестировать регистрацию, не отправляя реальные письма.
const sinon = require('sinon');
// --- Реальные модули (которые мы будем заменять) ---
const emailService = {
sendWelcomeEmail: function(userEmail) {
// Реальная отправка через SMTP
console.log(`Sending welcome email to ${userEmail}`);
return { success: true };
}
};
const userRepository = {
save: function(user) {
// Сохранение в БД
console.log(`Saving user ${user.email}`);
return { id: 1, ...user };
}
};
// --- Функция, которую мы тестируем ---
function registerUser(email, password) {
if (!email || !password) {
throw new Error('Email and password are required');
}
const user = userRepository.save({ email, password });
emailService.sendWelcomeEmail(email);
return user;
}
// --- Тест с использованием Mocha (но можно любой фреймворк) ---
describe('registerUser', function() {
let emailStub, saveStub;
beforeEach(function() {
// Создаём стабы для внешних зависимостей
emailStub = sinon.stub(emailService, 'sendWelcomeEmail').returns({ success: true });
saveStub = sinon.stub(userRepository, 'save').returns({ id: 1, email: 'test