При тестировании кода, использующего password-hash,
ключевая проблема заключается в том, что криптографические функции по
своей природе недетерминированы и вычислительно затратны. Хеширование
паролей включает соль, многократные итерации и внутренние случайные
компоненты, из-за чего прямое использование реальной библиотеки в
юнит-тестах приводит к нестабильности и замедлению выполнения тестового
набора.
Библиотека password-hash предоставляет два базовых
сценария:
Типичный код:
const passwordHash = require('password-hash');
const hash = passwordHash.generate('my_password');
const verified = passwordHash.verify('my_password', hash);
Фактически generate возвращает строку, содержащую
алгоритм, соль и хеш, а verify выполняет обратную проверку.
Это делает библиотеку удобной, но проблемной для изолированного
тестирования бизнес-логики.
При написании юнит-тестов важна изоляция логики от внешних
зависимостей. password-hash относится к таким зависимостям,
поскольку:
generate не является фиксированнымВместо этого в тестах используется контролируемая замена поведения.
Наиболее распространённый подход — полная подмена модуля:
jest.mock('password-hash', () => ({
generate: jest.fn(),
verify: jest.fn(),
}));
Далее задаётся поведение:
const passwordHash = require('password-hash');
passwordHash.generate.mockReturnValue('fixed_hash');
passwordHash.verify.mockImplementation((password, hash) => {
return password === 'correct_password' && hash === 'fixed_hash';
});
Такой подход делает тесты полностью детерминированными.
Иногда требуется сохранить часть реального поведения и заменить только генерацию:
jest.mock('password-hash', () => {
const actual = jest.requireActual('password-hash');
return {
...actual,
generate: jest.fn(() => 'static_hash'),
};
});
Это позволяет тестировать бизнес-логику вокруг хеша, но не сам алгоритм.
Более архитектурно устойчивый подход — не импортировать библиотеку напрямую, а передавать её как зависимость:
function createUserService(passwordHasher) {
return {
register(password) {
const hash = passwordHasher.generate(password);
return { passwordHash: hash };
},
login(password, storedHash) {
return passwordHasher.verify(password, storedHash);
}
};
}
Тест:
const fakeHasher = {
generate: (pwd) => `hash_${pwd}`,
verify: (pwd, hash) => hash === `hash_${pwd}`,
};
Такой способ полностью исключает необходимость мокирования модулей.
При использовании sinon создаются стабы:
const sinon = require('sinon');
const passwordHash = require('password-hash');
const generateStub = sinon.stub(passwordHash, 'generate').returns('stub_hash');
const verifyStub = sinon.stub(passwordHash, 'verify').returns(true);
После теста:
generateStub.restore();
verifyStub.restore();
При проверке логики логина важно контролировать не хеш, а результат проверки:
test('успешная авторизация', () => {
passwordHash.verify.mockReturnValue(true);
const result = authService.login('password', 'any_hash');
expect(result).toBe(true);
});
Здесь сам хеш не имеет значения, поскольку он выведен из теста как внешний фактор.
Распространённая ошибка — попытка частично проверять реальный алгоритм хеширования в юнит-тестах. Это приводит к:
Ещё одна проблема — чрезмерное упрощение моков:
verify: () => true
Такой вариант делает тест бесполезным, поскольку не проверяет корректность логики обработки результата.
Корректная структура тестируемого кода предполагает, что:
password-hash отвечает только за криптографиюtrue/false,
строки хеша)Это позволяет безопасно заменять реализацию библиотеки без переписывания тестов.
При тестировании регистрации пользователя часто требуется моделировать несколько состояний:
passwordHash.verify
.mockReturnValueOnce(true)
.mockReturnValueOnce(false);
Такой подход позволяет имитировать последовательные вызовы с разным поведением.
Если логика сервиса включает логирование или запись в базу,
мокирование password-hash позволяет отделить
криптографический слой от инфраструктуры:
const hash = passwordHash.generate('test');
database.save({ password: hash });
В тесте проверяется только факт вызова generate, а не
его внутреннее устройство.
Мокирование также используется для проверки реакции системы на изменения поведения зависимости:
passwordHash.verify.mockReturnValue(false);
Это позволяет проверять, как система реагирует на отказ верификации, не затрагивая криптографическую реализацию.
Чрезмерное использование мокирования password-hash
приводит к потере интеграционного покрытия. Поэтому:
password-hashТакое разделение сохраняет баланс между скоростью тестов и их достоверностью