Интеграционные тесты с реальной базой данных

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

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


Библиотека password-hash в JavaScript обычно используется как абстракция над криптографическими алгоритмами (bcrypt, argon2 или pbkdf2). На уровне интеграции важно учитывать:

  • неизменяемость результата хэширования при одинаковом входе и параметрах
  • невозможность восстановления исходного пароля из хэша
  • зависимость результата от “соли” (salt)
  • стабильность сравнения хэшей при аутентификации

В контексте базы данных эти свойства проверяются через полный цикл: создание пользователя → сохранение хэша → извлечение → проверка пароля.


Подготовка тестовой базы данных

Использование реальной базы данных в тестах требует строгой изоляции окружения. Обычно применяются следующие подходы:

1. Отдельная тестовая база

  • отдельный PostgreSQL / MySQL / MongoDB инстанс
  • изоляция через переменные окружения
  • использование отдельного пользователя БД

2. Контейнеризация

  • Docker Compose для поднятия БД
  • Testcontainers для автоматического управления жизненным циклом базы

3. In-memory решения (ограниченно)

  • SQLite in-memory режим
  • подходит только для простых сценариев, не отражает поведение продакшена полностью

Пример конфигурации окружения:

process.env.DB_HOST = "localhost";
process.env.DB_PORT = "5433";
process.env.DB_NAME = "test_db";
process.env.DB_USER = "test_user";
process.env.DB_PASSWORD = "test_password";

Структура тестируемого сценария

Типичный интеграционный тест для password-hash включает несколько этапов:

  1. Подготовка базы (очистка таблиц)
  2. Создание пользователя с паролем
  3. Хэширование пароля через password-hash
  4. Сохранение хэша в базе
  5. Извлечение записи
  6. Проверка пароля через compare-функцию
  7. Очистка данных после теста

Пример интеграции с библиотекой password-hash

import { hashPassword, verifyPassword } fr om "password-hash";
import { db } fr om "../db";

beforeEach(async () => {
  await db.query("DELETE FR OM users");
});

test("создание пользователя сохраняет хэш пароля", async () => {
  const password = "securePassword123";

  const hashed = await hashPassword(password);

  await db.query(
    "INS ERT IN TO users (email, password_hash) VALUES ($1, $2)",
    ["user@test.com", hashed]
  );

  const user = await db.query(
    "SEL ECT * FR OM users WH ERE email = $1",
    ["user@test.com"]
  );

  const isValid = await verifyPassword(password, user.rows[0].password_hash);

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

Проверка устойчивости хэшей

В интеграционных тестах важно убедиться, что хэш:

  • не изменяется при повторных чтениях из базы
  • корректно проходит проверку после сериализации/десериализации
  • не повреждается при изменении кодировки или ORM-слоя

Пример проверки стабильности:

test("хэш остается валидным после повторного чтения из БД", async () => {
  const password = "myPassword";

  const hashed = await hashPassword(password);

  await db.query(
    "INS ERT IN TO users (email, password_hash) VALUES ($1, $2)",
    ["stable@test.com", hashed]
  );

  const firstRead = await db.query(
    "SEL ECT password_hash FR OM users WH ERE email=$1",
    ["stable@test.com"]
  );

  const secondRead = await db.query(
    "SEL ECT password_hash FR OM users WH ERE email=$1",
    ["stable@test.com"]
  );

  const validFirst = await verifyPassword(password, firstRead.rows[0].password_hash);
  const validSecond = await verifyPassword(password, secondRead.rows[0].password_hash);

  expect(validFirst).toBe(true);
  expect(validSecond).toBe(true);
});

Работа с транзакциями в тестах

Использование транзакций позволяет ускорить тестирование и упростить откат состояния базы:

beforeEach(async () => {
  await db.query("BEGIN");
});

afterEach(async () => {
  await db.query("ROLLBACK");
});

Однако при тестировании password-hash важно учитывать, что некоторые ORM могут кешировать соединения, что приводит к неожиданному поведению при параллельных тестах.


Проверка сценариев аутентификации

Интеграционные тесты часто моделируют реальный логин пользователя:

test("аутентификация пользователя с корректным паролем", async () => {
  const password = "loginPass";

  const hashed = await hashPassword(password);

  await db.query(
    "INS ERT IN TO users (email, password_hash) VALUES ($1, $2)",
    ["auth@test.com", hashed]
  );

  const user = await db.query(
    "SEL ECT * FR OM users WH ERE email=$1",
    ["auth@test.com"]
  );

  const match = await verifyPassword(password, user.rows[0].password_hash);

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

Нагрузочные аспекты хэширования

Так как password-hash использует вычислительно дорогие алгоритмы, в интеграционных тестах учитывается:

  • время выполнения хэширования
  • влияние cost factor (bcrypt rounds)
  • параллельные запросы к базе

Иногда в тестовом окружении используют пониженный cost factor:

const hashed = await hashPassword(password, { rounds: 4 });

Это ускоряет тесты, но не должно использоваться в продакшене.


Изоляция тестовых данных

Критически важно избегать утечек данных между тестами:

  • уникальные email для каждого теста
  • очистка таблиц пользователей
  • сброс последовательностей ID
await db.query("TRUNCATE TABLE users RESTART IDENTITY");

Ошибочные сценарии

Интеграционные тесты должны проверять и негативные кейсы:

  • неверный пароль
  • отсутствующий пользователь
  • повреждённый хэш в базе
test("ошибка при неверном пароле", async () => {
  const hashed = await hashPassword("correct");

  await db.query(
    "INS ERT IN TO users (email, password_hash) VALUES ($1, $2)",
    ["fail@test.com", hashed]
  );

  const user = await db.query(
    "SELECT * FR OM users WH ERE email=$1",
    ["fail@test.com"]
  );

  const result = await verifyPassword("wrong", user.rows[0].password_hash);

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

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

Для максимальной близости к production-среде применяется Testcontainers:

import { PostgreSqlContainer } fr om "@testcontainers/postgresql";

let container;

beforeAll(async () => {
  container = await new PostgreSqlContainer().start();
  process.env.DB_URL = container.getConnectionUri();
});

afterAll(async () => {
  await container.stop();
});

Это позволяет запускать реальные СУБД в изолированных контейнерах.


Особенности работы с ORM

При использовании Prisma, TypeORM или Sequelize важно учитывать:

  • автоматическое преобразование типов может повлиять на хэш
  • скрытые преобразования строк
  • lazy loading может маскировать ошибки

Поэтому часто добавляют прямые SQL-запросы для критических проверок password-hash.


Типичные ошибки при интеграционном тестировании

  • использование моков вместо реальной БД
  • общий пользователь для всех тестов
  • отсутствие очистки данных
  • сравнение хэшей строково вместо verify-функции
  • игнорирование асинхронности хэширования

Каждая из этих ошибок приводит к ложным результатам тестирования и снижает надёжность проверки безопасности паролей.


Организация тестовой структуры проекта

Обычно структура выглядит следующим образом:

tests/
  integration/
    auth.test.js
    users.test.js
  setup/
    db.js
    teardown.js

Файл подключения БД:

export const db = new Pool({
  host: process.env.DB_HOST,
  port: process.env.DB_PORT,
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
});