Мокирование password-hash в юнит-тестах

При тестировании кода, использующего 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

Наиболее распространённый подход — полная подмена модуля:

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'),
  };
});

Это позволяет тестировать бизнес-логику вокруг хеша, но не сам алгоритм.

Использование dependency injection

Более архитектурно устойчивый подход — не импортировать библиотеку напрямую, а передавать её как зависимость:

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

При использовании 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
  • критические сценарии проверяют совместную работу компонентов

Такое разделение сохраняет баланс между скоростью тестов и их достоверностью