Модульное тестирование функций регистрации и логина

Роль хеширования паролей в тестируемой архитектуре

В системах аутентификации ключевым элементом является преобразование пароля в необратимое значение. Библиотека password-hash предоставляет простой механизм создания и проверки хешей, который удобно использовать в учебных и небольших проектах.

Основные операции:

  • генерация хеша пароля при регистрации пользователя;
  • проверка соответствия введённого пароля сохранённому хешу при логине.

При модульном тестировании важно изолировать эти операции от внешних зависимостей (база данных, HTTP-слой), чтобы проверять только бизнес-логику.


Базовая реализация функций регистрации и логина

Логика обычно разделяется на два слоя: сервис аутентификации и слой хранения данных.

Пример упрощённого сервиса:

const passwordHash = require('password-hash');

class AuthService {
  constructor(userRepository) {
    this.userRepository = userRepository;
  }

  register(username, password) {
    const existing = this.userRepository.findByUsername(username);
    if (existing) {
      throw new Error('User already exists');
    }

    const hashedPassword = passwordHash.generate(password);

    const user = {
      id: Date.now(),
      username,
      password: hashedPassword
    };

    this.userRepository.save(user);
    return user;
  }

  login(username, password) {
    const user = this.userRepository.findByUsername(username);
    if (!user) {
      throw new Error('User not found');
    }

    const isValid = passwordHash.verify(password, user.password);
    if (!isValid) {
      throw new Error('Invalid password');
    }

    return user;
  }
}

module.exports = AuthService;

Подготовка к модульному тестированию

Модульные тесты должны проверять:

  • корректное создание пользователя;
  • корректное хеширование пароля;
  • успешный и неуспешный логин;
  • обработку ошибок;
  • отсутствие зависимости от реальной базы данных.

Для этого используется мок-репозиторий:

class MockUserRepository {
  constructor() {
    this.users = [];
  }

  findByUsername(username) {
    return this.users.find(u => u.username === username);
  }

  save(user) {
    this.users.push(user);
  }
}

module.exports = MockUserRepository;

Тестирование регистрации пользователя

Тест должен проверять, что пароль не сохраняется в открытом виде и пользователь корректно создаётся.

Пример на Jest:

const AuthService = require('./AuthService');
const MockUserRepository = require('./MockUserRepository');
const passwordHash = require('password-hash');

test('регистрация создаёт пользователя с хешированным паролем', () => {
  const repo = new MockUserRepository();
  const auth = new AuthService(repo);

  const user = auth.register('alice', '123456');

  expect(user.username).toBe('alice');
  expect(user.password).not.toBe('123456');
  expect(passwordHash.verify('123456', user.password)).toBe(true);
});

Ключевая проверка — использование passwordHash.verify, а не прямое сравнение строк.


Тестирование дубликатов пользователей

Важно проверить защиту от повторной регистрации:

test('нельзя зарегистрировать пользователя с уже существующим username', () => {
  const repo = new MockUserRepository();
  const auth = new AuthService(repo);

  auth.register('alice', '123456');

  expect(() => {
    auth.register('alice', '999999');
  }).toThrow('User already exists');
});

Тестирование логина с корректным паролем

test('успешный логин с правильным паролем', () => {
  const repo = new MockUserRepository();
  const auth = new AuthService(repo);

  auth.register('alice', '123456');

  const user = auth.login('alice', '123456');

  expect(user.username).toBe('alice');
});

Здесь важно, что логин проходит через проверку хеша, а не через прямое сравнение.


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

test('логин с неверным паролем вызывает ошибку', () => {
  const repo = new MockUserRepository();
  const auth = new AuthService(repo);

  auth.register('alice', '123456');

  expect(() => {
    auth.login('alice', 'wrong-password');
  }).toThrow('Invalid password');
});

Тестирование отсутствующего пользователя

test('логин несуществующего пользователя', () => {
  const repo = new MockUserRepository();
  const auth = new AuthService(repo);

  expect(() => {
    auth.login('ghost', '123456');
  }).toThrow('User not found');
});

Изоляция тестов и детерминированность

При работе с password-hash важно учитывать:

  • хеширование может включать соль, поэтому результат всегда различен;
  • тесты не должны сравнивать хеши напрямую;
  • проверка выполняется только через verify.

Принцип:

  • ❌ неверно: expect(user.password).toBe('123456')
  • ✔ правильно: passwordHash.verify(password, user.password)

Тестирование граничных случаев

Дополнительно проверяются нестандартные сценарии:

test('регистрация с пустым паролем', () => {
  const repo = new MockUserRepository();
  const auth = new AuthService(repo);

  expect(() => {
    auth.register('alice', '');
  }).toThrow();
});
test('регистрация с длинным паролем', () => {
  const repo = new MockUserRepository();
  const auth = new AuthService(repo);

  const longPassword = 'a'.repeat(1000);
  const user = auth.register('alice', longPassword);

  expect(passwordHash.verify(longPassword, user.password)).toBe(true);
});

Использование моков и контроль состояния

Модульные тесты требуют полного контроля над данными. Мок-репозиторий обеспечивает:

  • предсказуемое состояние;
  • отсутствие зависимости от БД;
  • ускорение тестов.

Иногда добавляется очистка состояния:

afterEach(() => {
  repo.users = [];
});

Проверка целостности цепочки регистрации и логина

Интеграция двух операций в одном тесте:

test('полный цикл регистрации и логина', () => {
  const repo = new MockUserRepository();
  const auth = new AuthService(repo);

  auth.register('alice', '123456');
  const loggedUser = auth.login('alice', '123456');

  expect(loggedUser.username).toBe('alice');
});

Ошибки проектирования, выявляемые тестами

Модульные тесты часто обнаруживают:

  • сохранение пароля в открытом виде;
  • отсутствие проверки существующего пользователя;
  • некорректную обработку undefined пользователя;
  • неправильную работу verify.

Такие дефекты выявляются без запуска всего приложения, только на уровне изолированного модуля.


Тестирование стабильности хеширования

Хотя password-hash скрывает реализацию соли, важно убедиться, что:

  • одинаковые пароли дают валидный verify;
  • разные пароли не совпадают при проверке;
  • хеш всегда является строкой.
test('хеш всегда строка', () => {
  const hash = passwordHash.generate('123456');
  expect(typeof hash).toBe('string');
});