bcrypt используется для криптографического хеширования паролей и
построен вокруг дорогой по вычислениям функции, что делает его медленным
по своей природе. Именно эта особенность становится ключевой проблемой в
тестовой среде: полноценные вызовы bcrypt.hash() и
bcrypt.compare() могут существенно замедлять выполнение
тестов, нарушать детерминированность и усложнять изоляцию
бизнес-логики.
В тестах почти никогда не требуется проверять сам алгоритм хеширования. Его корректность уже гарантирована библиотекой и многолетним использованием в индустрии. Тесты должны проверять поведение приложения, а не криптографические примитивы.
Основная причина замены bcrypt в тестах — контроль над средой выполнения и скоростью.
bcrypt использует вычислительно затратные операции (work factor / salt rounds). Даже при минимальных настройках:
bcrypt.hash(password, 10)
операция остаётся относительно медленной, особенно при массовом запуске тестов.
Проблемы, которые возникают без мокирования:
Кроме того, bcrypt возвращает разные хеши даже для одинакового входа из-за соли. Это делает невозможным прямое сравнение значений в тестах.
Мок bcrypt уместен в следующих случаях:
Когда bcrypt не следует мокировать:
Иначе можно получить ситуацию, когда тесты проходят, но реальная система ломается.
Существует несколько устойчивых стратегий, каждая из которых подходит под разные архитектурные решения.
Самый распространённый подход — замена bcryptjs через
механизмы тестового фреймворка.
Пример с Jest:
jest.mock('bcryptjs', () => ({
hash: jest.fn(),
compare: jest.fn()
}));
Далее задаётся поведение:
const bcrypt = require('bcryptjs');
bcrypt.hash.mockResolvedValue('hashed_password');
bcrypt.compare.mockResolvedValue(true);
Такой подход позволяет:
Минус заключается в жёсткой привязке тестов к реализации модуля.
Более стабильный подход — создание собственной упрощённой версии bcrypt.
// __mocks__/bcryptjs.js
module.exports = {
hash: async (password) => `hashed_${password}`,
compare: async (password, hash) => hash === `hashed_${password}`
};
Преимущества:
Недостатки:
Этот вариант хорошо подходит для unit-тестов бизнес-логики.
Наиболее гибкий и архитектурно правильный способ — не мокировать библиотеку напрямую, а абстрагировать её через сервис.
Пример:
class AuthService {
constructor(hasher) {
this.hasher = hasher;
}
async register(password) {
const hash = await this.hasher.hash(password);
return hash;
}
async login(password, hash) {
return this.hasher.compare(password, hash);
}
}
В реальном коде:
const bcrypt = require('bcryptjs');
const bcryptAdapter = {
hash: bcrypt.hash,
compare: bcrypt.compare
};
В тестах:
const fakeHasher = {
hash: async (p) => `hashed_${p}`,
compare: async (p, h) => h === `hashed_${p}`
};
Такой подход:
bcrypt возвращает Promise. Частая ошибка — синхронные моки:
bcrypt.hash.mockReturnValue('hash'); // ошибка
Правильно:
bcrypt.hash.mockResolvedValue('hash');
Хеш нельзя использовать как стабильное значение:
expect(await bcrypt.hash('123', 10)).toBe('some_hash'); // плохой тест
Причина — соль и внутренние параметры алгоритма.
Часто мокают hash, но забывают про compare.
Это приводит к несоответствию поведения логики аутентификации.
Это приводит к:
Полное исключение bcrypt из тестов тоже является ошибкой. Правильная стратегия — разделение уровней тестирования:
Такой подход обеспечивает одновременно:
Иногда требуется имитировать более сложное поведение bcrypt:
Пример эмуляции ошибки:
bcrypt.hash.mockRejectedValue(new Error('hash error'));
Пример задержки:
bcrypt.compare.mockImplementation(
() => new Promise(resolve => setTimeout(() => resolve(true), 50))
);
Это позволяет тестировать:
Правильное мокирование часто выявляет архитектурные проблемы. Если bcrypt сложно заменить в тестах, это сигнал о:
Решение почти всегда одно — вынести криптографию в отдельный слой.
Прямой мок:
Manual mock:
Dependency Injection:
bcrypt специально спроектирован так, чтобы исключить предсказуемость выходных данных. Это делает его надёжным с точки зрения безопасности, но неудобным для тестирования.
Мокирование в этом контексте — не обходной путь, а обязательная часть стратегии тестирования, позволяющая отделить криптографию от логики приложения.
При правильной организации тестов bcrypt перестаёт быть источником сложности и превращается в заменяемый инфраструктурный компонент, не влияющий на проверку бизнес-правил.