Юнит-тестирование функций хеширования

Хеширование паролей с помощью bcrypt.js обладает ключевым свойством — недетерминированностью результата при одинаковом входном значении. Это напрямую влияет на подход к юнит-тестированию: сравнение хешей строка-в-строку невозможно без контроля параметров соли.

bcrypt.js использует встроенную генерацию соли, которая добавляет случайность в каждый результат хеширования. Поэтому базовая проверка вида «хеш равен ожидаемой строке» неприменима в большинстве сценариев.

Базовая структура тестируемых функций

В приложениях обычно выделяются две операции:

  • генерация хеша пароля
  • сравнение пароля с хешем
import bcrypt from 'bcryptjs';

export async function hashPassword(password) {
  const saltRounds = 10;
  return bcrypt.hash(password, saltRounds);
}

export async function comparePassword(password, hash) {
  return bcrypt.compare(password, hash);
}

Юнит-тестирование таких функций требует разделения проверки на поведенческий уровень, а не на уровне конкретного значения строки.

Проверка функции генерации хеша

Прямое сравнение результата хеширования невозможно, поэтому тестирование строится вокруг свойств результата.

Основные проверяемые свойства:

  • результат всегда строка
  • результат соответствует формату bcrypt-хеша
  • два вызова с одинаковым входом дают разные строки (при разной соли)
  • хеш можно валидно использовать в compare

Пример теста:

import { hashPassword } from './auth';

test('hashPassword возвращает строку', async () => {
  const hash = await hashPassword('secret123');

  expect(typeof hash).toBe('string');
});

Проверка структуры хеша:

test('bcrypt hash имеет корректный формат', async () => {
  const hash = await hashPassword('secret123');

  expect(hash).toMatch(/^\$2[aby]\$\d{2}\$.+/);
});

Этот паттерн проверяет соответствие стандарту bcrypt: версия алгоритма, cost factor и соль.

Изоляция нестабильности через контроль соли

Для получения детерминированного результата можно использовать фиксированную соль.

import bcrypt from 'bcryptjs';

test('детерминированное хеширование с фиксированной солью', () => {
  const salt = '$2a$10$abcdefghijklmnopqrstuv';

  const hash1 = bcrypt.hashSync('password', salt);
  const hash2 = bcrypt.hashSync('password', salt);

  expect(hash1).toBe(hash2);
});

Это позволяет проверять внутреннюю логику без влияния случайности, однако такой подход применим только в тестах.

Тестирование сравнения пароля

Функция сравнения — основной объект юнит-тестирования, так как она детерминирована при корректном хеше.

test('comparePassword возвращает true для корректного пароля', async () => {
  const password = 'myPassword';
  const hash = await bcrypt.hash(password, 10);

  const result = await bcrypt.compare(password, hash);

  expect(result).toBe(true);
});

Негативный сценарий:

test('comparePassword возвращает false для неверного пароля', async () => {
  const hash = await bcrypt.hash('correct', 10);

  const result = await bcrypt.compare('wrong', hash);

  expect(result).toBe(false);
});

Работа с асинхронностью в тестах

bcrypt.js использует асинхронные операции, поэтому тесты должны корректно обрабатывать Promise.

Типичные подходы:

  • async/await
  • возврат Promise из теста
  • использование done в старых тестовых раннерах
test('асинхронное сравнение пароля', async () => {
  const hash = await bcrypt.hash('12345', 10);
  const isValid = await bcrypt.compare('12345', hash);

  expect(isValid).toBe(true);
});

Мокирование bcrypt для изоляции бизнес-логики

В более сложных системах bcrypt часто заменяется моками для ускорения тестов и устранения зависимости от CPU-операций.

Пример с Jest:

jest.mock('bcryptjs', () => ({
  hash: jest.fn(() => Promise.resolve('hashed_password')),
  compare: jest.fn(() => Promise.resolve(true)),
}));

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

Пример проверки вызова:

import bcrypt from 'bcryptjs';
import { hashPassword } from './auth';

test('bcrypt.hash вызывается с корректными параметрами', async () => {
  await hashPassword('test');

  expect(bcrypt.hash).toHaveBeenCalledWith('test', 10);
});

Проверка устойчивости к одинаковым входным данным

Несмотря на различие хешей, важно проверять, что bcrypt сохраняет корректность проверки:

test('одинаковый пароль проходит проверку с разными хешами', async () => {
  const password = 'samePassword';

  const hash1 = await bcrypt.hash(password, 10);
  const hash2 = await bcrypt.hash(password, 10);

  const result1 = await bcrypt.compare(password, hash1);
  const result2 = await bcrypt.compare(password, hash2);

  expect(result1).toBe(true);
  expect(result2).toBe(true);
});

Тестирование граничных значений

Особое внимание требуется к:

  • пустым строкам
  • очень длинным паролям
  • Unicode-символам
  • пробелам
test('поддержка Unicode паролей', async () => {
  const password = 'пароль?';

  const hash = await bcrypt.hash(password, 10);
  const result = await bcrypt.compare(password, hash);

  expect(result).toBe(true);
});
test('пустой пароль корректно обрабатывается', async () => {
  const hash = await bcrypt.hash('', 10);
  const result = await bcrypt.compare('', hash);

  expect(result).toBe(true);
});

Производительность в тестовой среде

bcrypt специально замедлен для повышения безопасности, поэтому в тестах часто уменьшают cost factor.

const TEST_SALT_ROUNDS = 4;

Использование низкого значения позволяет ускорить выполнение тестов без изменения логики проверки.

Ошибочные сценарии и обработка исключений

Важно тестировать некорректные входные данные:

test('compare возвращает false при невалидном хеше', async () => {
  const result = await bcrypt.compare('password', 'invalid_hash');

  expect(result).toBe(false);
});

Также возможна проверка выброса ошибок при критических сбоях (в зависимости от реализации обёртки над bcrypt).

Изоляция криптографии от бизнес-логики

В архитектуре тестирования bcrypt чаще рассматривается как внешняя зависимость. Это приводит к следующим практикам:

  • тестируется только взаимодействие с API bcrypt
  • бизнес-логика отделяется от криптографических деталей
  • сравнение происходит через поведение, а не через значения

Такой подход снижает хрупкость тестов при изменении алгоритма или параметров соли.