Опирается на правила:
NODETEST-15…NODETEST-18из Node Test Strategy → раздел 5. Структура одного теста.
Важно знать
- Тест использует общий setup-хелпер (app + preparer), не собирает TestingModule руками в каждом файле.
- Имена:
it('<action> when <condition> — <expected>'), BR-код — вdescribeилиit.- HTTP через
supertest(app.getHttpServer())— реальный стек Nest, полный контроль над методом, заголовками, телом.- JWT через хелпер (
successToken(),customerToken(...)), не токены руками.- AAA-структура (Arrange / Act / Assert) с пустой строкой между блоками.
- Assert проверяет и ответ, и состояние БД — не только status-код.
- DatabasePreparer готовит состояние Arrange;
clear*()вызывается вbeforeEachчерез фабрику.
Полный пример
import * as request from 'supertest';
import { createOrderApp } from '../support/app.factory';
import { OrderDatabasePreparer } from '../support/order-database-preparer';
import { OrderBuilder } from '../support/order-builder';
import { successToken } from '../support/auth.helpers';
import { OrderStatus } from '../../src/order/domain/order-status';
describe('POST /v1/orders/:id/confirm', () => {
let app: INestApplication;
let preparer: OrderDatabasePreparer;
beforeAll(async () => {
({ app, preparer } = await createOrderApp());
await app.init();
});
afterAll(async () => {
await app.close();
});
beforeEach(async () => {
await preparer.clearOrders();
});
it('confirms order when status is DRAFT — returns 200 and outbox event', async () => {
// Arrange
const orderId = '11111111-1111-1111-1111-111111111111';
const order = new OrderBuilder()
.withId(orderId)
.withStatus(OrderStatus.DRAFT)
.build();
await preparer.createOrder(order).prepare();
// Act
const response = await request(app.getHttpServer())
.post(`/v1/orders/${orderId}/confirm`)
.set('Authorization', `Bearer ${successToken()}`)
.send();
// Assert
expect(response.status).toBe(200);
expect(response.body.orderId).toBe(orderId);
const outbox = await preparer.findOutboxEvents();
expect(outbox).toHaveLength(1);
expect(outbox[0].eventType).toBe('OrderConfirmed');
});
});
Что есть:
createOrderApp()возвращает собранное приложение и preparer — TestingModule не собирается руками.beforeEachчистит только нужные таблицы черезpreparer.clearOrders().- AAA-структура с пустыми строками между блоками.
- Assert проверяет и ответ, и side-effect (Outbox-таблицу).
Фабрика приложения
NODETEST-15: один platform-хелпер на сервис.
// test/support/app.factory.ts
export async function createOrderApp(): Promise<{
app: INestApplication;
preparer: OrderDatabasePreparer;
}> {
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(CLOCK)
.useValue(fixedClock)
.overrideProvider(UUID_PROVIDER)
.useValue(fixedUuid)
.compile();
const app = moduleRef.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
const dataSource = moduleRef.get<DataSource>(DataSource);
const preparer = new OrderDatabasePreparer(dataSource);
return { app, preparer };
}
Что даёт фабрика:
- Единый DI-override для
CLOCKиUUID_PROVIDER— детерминизм гарантирован для всех тестов. - Единая конфигурация
ValidationPipeи прочих глобальных middleware. preparerсоздаётся из того жеDataSource, что и само приложение — нет отдельного соединения.
Не дублируем Test.createTestingModule(...) в каждом describe-блоке: любое расхождение в конфиге делает тесты неконсистентными.
Имена тестов
NODETEST-16: формат it('<action> when <condition> — <expected>').
// Хорошо
it('confirms order when status is DRAFT — returns 200')
it('confirms order when reservation failed — returns 409 OUT_OF_STOCK')
it('creates order when email is invalid — returns 400')
it('gets order when not found — returns 404')
it('cancels order when already cancelled — returns 409')
BR-код — в describe или it:
describe('BR-002: confirm order', () => {
it('fails when reservation failed — returns 409', async () => { ... });
it('succeeds when status is DRAFT — returns 200', async () => { ... });
});
// Или прямо в it — когда describe охватывает одно правило
it('BR-002: confirm fails when reservation failed — returns 409', async () => { ... });
BR-код (BR-002) берётся из спецификации Bounded Context (см. Use Case Pattern). Это создаёт прямую трассируемость: спека → тест → код. Изменили BR-002 — сразу видно, какие тесты нужно обновить.
Плохие имена — то, что ничего не говорит:
// Плохо
it('test1')
it('testCancel')
it('should work')
it('order endpoint')
supertest — HTTP с полным контролем
NODETEST-17: вызов через supertest(app.getHttpServer()).
const response = await request(app.getHttpServer())
.post(`/v1/orders/${orderId}/confirm`)
.set('Authorization', `Bearer ${successToken()}`)
.set('X-Idempotency-Key', idempotencyKey)
.send({ note: 'срочно' });
Что есть:
.post(url)— явный HTTP-метод..set(header, value)— точный контроль заголовков..send(body)— тело запроса.- Ответ —
response.status,response.body,response.headers.
Полный пример с проверкой ответа:
const response = await request(app.getHttpServer())
.post(`/v1/orders/${orderId}/confirm`)
.set('Authorization', `Bearer ${successToken()}`)
.send();
expect(response.status).toBe(409);
expect(response.body.code).toBe('OUT_OF_STOCK');
expect(response.headers['content-type']).toMatch(/application\/json/);
supertest vs TestRestTemplate
| Инструмент | Когда |
|---|---|
supertest | Integration-тест с реальным NestJS context + PostgreSQL |
supertest + in-memory фейк | Unit-тест контроллера: overrideProvider порта-репозитория на фейк, без БД |
supertest работает с реальным HTTP-сервером Nest — проходит весь стек: guards, pipes, interceptors, exception filters. Это и есть нужный уровень интеграции.
JWT через хелперы
NODETEST-18: токены через хелперы, не руками.
// Хорошо
import { successToken, customerToken, adminToken } from '../support/auth.helpers';
.set('Authorization', `Bearer ${successToken()}`)
.set('Authorization', `Bearer ${customerToken(customerId)}`)
.set('Authorization', `Bearer ${adminToken()}`)
// Плохо — токен захардкожен
.set('Authorization', 'Bearer eyJhbGciOiJSUzI1NiJ9...')
// Плохо — сборка токена в каждом тесте
const payload = { sub: userId, role: 'customer' };
const token = jwt.sign(payload, testSecret);
.set('Authorization', `Bearer ${token}`)
Хелперы:
successToken()— generic токен с ролью customer.adminToken()— роль admin.customerToken(customerId)—sub = customerId, роль customer.sellerToken(sellerId)— роль seller,sub = sellerId.
Единый source-of-truth: структура токена меняется в одном месте. Тесты на ABAC — явно передаём разные sub через хелпер, не угадываем строки.
Хелпер создаёт токен, совместимый с фейковым JWT-валидатором, настроенным в фабрике (overrideProvider guard'а). Подробнее — в node/testing-module.
Полный пример — негативный сценарий
describe('BR-002: confirm order', () => {
it('fails when reservation failed — returns 409 OUT_OF_STOCK', async () => {
// Arrange
const orderId = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
const order = new OrderBuilder()
.withId(orderId)
.withStatus(OrderStatus.RESERVATION_FAILED)
.build();
await preparer.createOrder(order).prepare();
// Act
const response = await request(app.getHttpServer())
.post(`/v1/orders/${orderId}/confirm`)
.set('Authorization', `Bearer ${successToken()}`)
.send();
// Assert
expect(response.status).toBe(409);
expect(response.body.code).toBe('OUT_OF_STOCK');
const orders = await preparer.findOrders({ id: orderId });
expect(orders).toHaveLength(1);
expect(orders[0].status).toBe('RESERVATION_FAILED');
});
});
Assert здесь двойной:
- Ответ содержит правильный error-код.
- БД не изменилась — статус остался
RESERVATION_FAILED. Это проверяет, что Handler не записал частичное изменение.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
Test.createTestingModule(...) в каждом describe руками | NODETEST-15 | createOrderApp() фабрика |
it('test1'), it('should work') | NODETEST-16 | it('<action> when <condition> — <expected>') |
Нет BR-кода в describe/it | NODETEST-16 | describe('BR-002: ...') или it('BR-002: ...') |
axios.get(...) / fetch(...) вместо supertest | NODETEST-17 | request(app.getHttpServer()).get(...) |
| Захардкоженный JWT-токен в тесте | NODETEST-18 | successToken() / customerToken(id) |
Сборка токена через jwt.sign(...) в тесте | NODETEST-18 | хелперы из auth.helpers.ts |
| Assert только на status-код, без проверки БД | NODETEST-15 | проверять side-effects: Outbox, статус записи |
Один большой it на весь lifecycle заказа | NODETEST-3 | один it = один сценарий |
Куда дальше
- node/basics.md — базовые правила: синхронность, детерминизм, AAA.
- node/testing-module.md — фабрика TestingModule, DI-overrides, JWT-guard.
- node/database-preparer.md —
OrderDatabasePreparer:clear*(),create*(),prepare(). - node/fluent-builders.md —
OrderBuilderсwith*()иbuild(). - node/no-kafka-redis-async.md — почему Kafka не поднимаем, Outbox в Assert.
- Use Case Pattern — спецификация — откуда берутся BR-коды.