Какие подходы и инструменты лучше использовать для написания e2e-тестов бэкенда на NestJS?

Как правильно оформить e2e‑тесты для эндпоинта POST /auth/register в NestJS, чтобы один тест проверял успешную регистрацию, а другой — возвращал 400 при некорректном email? Ниже приведён текущий код теста:
  • 5 мая 2026 г. 13:02
Ответы на вопрос 3
Коротко — что обычно используют и почему
- Jest + Supertest — стандарт для e2e в NestJS: Jest как раннер/ассерты, Supertest для HTTP-запросов к запущенному приложению.
- Запуск реального Nest-приложения (Test.createTestingModule -> module.createNestApplication()) с app.init() — чтобы тесты проходили через все middleware, guards и pipes.
- ValidationPipe глобально в тестовом приложении — чтобы проверка DTO работала так же, как в проде.
- Тестовая БД: либо отдельная тестовая база (Postgres/MySQL), либо in-memory (SQLite), либо контейнеры (testcontainers). Для Prisma/TypeORM — очищайте БД между тестами (deleteMany / repository.clear / транзакции).
- Моки для внешних сервисов (email, queue, SSO) — заменяйте реальные провайдеры заглушками через overrideProvider.
- Не держите state между тестами: чистите БД в beforeEach/afterEach или создавайте транзакции + rollback.

Конкретный пример e2e‑теста для POST /auth/register (Jest + Supertest). Предположим, что:
- У вас AppModule,
- DTO для регистрации использует class-validator (@IsEmail, @IsString, @MinLength и т.д.),
- Вы используете TypeORM (далее также покажу Prisma-паттерн).

Пример для TypeORM

tests/auth.e2e-spec.ts
(схема — минимальна, вставьте путь к AppModule и сущности/репозиторию вашего проекта)
```ts
import request from 'supertest';
import { Test } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import { getConnection, Repository } from 'typeorm';
import { AppModule } from '../src/app.module';
import { User } from '../src/users/user.entity';
import { getRepositoryToken } from '@nestjs/typeorm';

describe('AuthController (e2e)', () => {
  let app: INestApplication;
  let userRepo: Repository<User>;

  beforeAll(async () => {
    const moduleRef = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleRef.createNestApplication();
    // Включаем валидацию как в проде
    app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }));
    await app.init();

    userRepo = moduleRef.get<Repository<User>>(getRepositoryToken(User));
  });

  afterAll(async () => {
    await getConnection().close();
    await app.close();
  });

  beforeEach(async () => {
    // чистим таблицу пользователей между тестами
    await userRepo.query(`TRUNCATE TABLE "user" RESTART IDENTITY CASCADE`);
    // либо: await userRepo.clear();
  });

  it('POST /auth/register - success', async () => {
    const payload = {
      email: `user${Date.now()}@example.com`,
      password: 'strongPassword123',
      name: 'User Name',
    };

    const res = await request(app.getHttpServer())
      .post('/auth/register')
      .send(payload)
      .expect(201);

    // базовые проверки ответа
    expect(res.body).toHaveProperty('id');
    expect(res.body.email).toBe(payload.email);
    expect(res.body).not.toHaveProperty('password'); // пароль не должен возвращаться
  });

  it('POST /auth/register - invalid email -> 400', async () => {
    const payload = {
      email: 'not-an-email',
      password: 'strongPassword123',
      name: 'User Name',
    };

    const res = await request(app.getHttpServer())
      .post('/auth/register')
      .send(payload)
      .expect(400);

    // Проверяем формат ошибки, может отличаться в вашем проекте
    expect(res.body).toHaveProperty('statusCode', 400);
    expect(res.body).toHaveProperty('message');
    // Обычно message содержит массив строк ошибок валидации
    expect(res.body.message.some((m: string) => m.toLowerCase().includes('email'))).toBeTruthy();
  });
});
```

Примечания к коду:
- ValidationPipe в тесте — критично, иначе некорректный email не даст 400.
- Проверки в expect(...) подстраивайте под формат ошибок вашего приложения (Nest стандартно возвращает {statusCode, message, error}).
- TRUNCATE/clear нужен, чтобы тесты были изолированы. В PostgreSQL удобно TRUNCATE ... CASCADE. Для SQLite/Prisma — другие команды.

Если вы используете Prisma

- В beforeAll получите PrismaService (или client) и между тестами делайте await prisma.user.deleteMany() для очистки.
- В AppModule для e2e можно overrideProvider(EmailService).useValue(mockMail) чтобы не шлём реальные письма.

Пример очистки для Prisma:
```ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// ...
beforeEach(async () => {
  await prisma.user.deleteMany();
});
afterAll(async () => {
  await prisma.$disconnect();
});
```

Советы и распространённые ошибки
- Убедитесь, что DTO использует @IsEmail(), иначе валидация не сработает.
- Если ваш pipeline использует transform: true (ValidationPipe), в тестах можно включить transform, если нужен типизированный body.
- Мокайте внешние ресурсы (email, redis, внешние API) — e2e лучше проверяет HTTP-поток и интеграцию с БД, но внешние сервисы должны быть под контролем.
- Используйте отдельную БД/контейнер для тестов, чтобы не портить dev/production данные.
- Не забывайте про таймауты Jest для медленных интеграционных тестов (jest.setTimeout или configuration).

Если хотите — пришлите ваш текущий код теста и AppModule (или DTO/Controller) — помогу адаптировать и показать точный исправленный пример под ваш проект.
Нет, тестировать всё подряд в e2e не нужно — для этого есть unit-тесты. На e2e уровне достаточно основных флоу: успешная регистрация, успешный логин, и пару критических ошибок (дубль, невалидный формат). Валидацию всех полей (длины, регексы, типы) лучше покрывать unit-тестами — там это дешевле и не поднимает весь AppModule.
Рассматривай e2e тесты в контексте всего проекта (подумай о тестировщиках-людях и о том, как заказчик потом будет принимать работу) и подумай о том, какую вообще проблему они решают. <br/> <br/> В конечном итоге ты стремишься к вот такой ситуции: <br/> 1. Все бизнес-сценарии, которые описаны в спецификации реализованы именно так, как они описаны в спецификации. <br/> 2. То что не описано в спецификации - не приводит к угрозам безопасности или неисправимым ошибкам. <br/> <br/> Обрати внимание, что тут ни слова о nest и всём что может вернуть бэкенд нет. <br/> <br/> 1. Чем меньше денег компания на это потратит, тем лучше. <br/> 2. Затраты на тестирование соразмерны вохможным убыткам от ошибок, которые можно было бы пропустить.
Похожие вопросы