Опирается на правила:
NODETEST-1,NODETEST-2,NODETEST-3,NODETEST-X1,NODETEST-X2из Node Test Strategy → раздел 1. Базовые правила.
Важно знать
- Тест должен быть быстрым и детерминированным.
setTimeout-ожидание и polling — признак недетерминированного дизайна, не способ «дать событию время».- Интеграционный тест =
Test.createTestingModule({...}).compile()+app.init()+ реальный PostgreSQL (testcontainers-node) + HTTP-вызов через supertest.- Внешние HTTP-сервисы (платёж, каталог, логистика) —
nockилиmsw; не трогаем реальные endpoints.- Kafka и Redis не поднимаем — конфиг
integration-testих отключает; события проверяются через Outbox-таблицу.- Время и UUID — кастомные провайдеры с токенами
CLOCKиUUID_PROVIDER; в тесте —.overrideProvider(CLOCK).useValue(fixedClock).- Один тест — один сценарий, AAA-структура (
// Arrange,// Act,// Assert).- Jest — дефолт; Vitest допустим, API override провайдеров и fake timers эквивалентны.
UCP-подход к тестам в Node строится вокруг той же идеи, что и в Java: тест быстрый (миллисекунды, не секунды), детерминированный (одинаковый результат при каждом прогоне), простой (один сценарий). Разница в инструментах: вместо @SpringBootTest — Test.createTestingModule, вместо @MockitoBean — .overrideProvider, вместо Awaitility — его полное отсутствие.
Что входит в интеграционный тест
NODETEST-1: полный стек внутри сервиса, минус внешние системы.
| Часть | Где в тесте |
|---|---|
| NestJS application | Test.createTestingModule({...}).compile() + app.init() |
| PostgreSQL | PostgreSqlContainer (testcontainers-node), globalSetup |
| HTTP-клиент | supertest(app.getHttpServer()).post(...) |
| Внешние HTTP | nock или msw (node-интерсептор) |
| Kafka | НЕ поднимаем; проверяем Outbox-таблицу через DatabasePreparer |
| Redis | НЕ поднимаем; конфиг integration-test отключает кеш |
| Время | overrideProvider(CLOCK).useValue(fixedClock) |
| UUID | overrideProvider(UUID_PROVIDER).useValue(fixedUuidProvider) |
Это даёт:
- End-to-end внутри сервиса — от HTTP-запроса до строки в PostgreSQL.
- Без асинхронных рассинхронизаций — нет ожидания Kafka consumer-а.
- Время прогона — один тест занимает 30–150 мс; контейнер поднимается один раз в
globalSetup.
it('BR-001: confirm order when DRAFT — returns 200 and creates outbox event', async () => {
const orderId = '11111111-1111-1111-1111-111111111111';
const now = new Date('2026-05-26T10:00:00.000Z');
// Arrange
await preparer.createOrder({ id: orderId, status: 'DRAFT', customerId: 'c1' }).prepare();
// Act
const response = await request(app.getHttpServer())
.post(`/v1/orders/${orderId}/confirm`)
.set('Authorization', `Bearer ${successToken()}`);
// Assert
expect(response.status).toBe(200);
const outbox = await dataSource.query('SELECT event_type FROM outbox WHERE aggregate_id = $1', [orderId]);
expect(outbox).toHaveLength(1);
expect(outbox[0].event_type).toBe('OrderConfirmed');
});
Все тесты детерминированные
NODETEST-2: никаких await new Promise(r => setTimeout(r, ...)), polling-циклов и new Date()/randomUUID() в домене напрямую.
// ПЛОХО — тест зависит от скорости CI-машины
await new Promise(r => setTimeout(r, 500));
const order = await orderRepository.findById(orderId);
expect(order).toBeDefined();
// ПЛОХО — polling скрывает недетерминированный дизайн
let order;
while (!order) {
order = await orderRepository.findById(orderId);
await new Promise(r => setTimeout(r, 100));
}
Что плохо:
- Нестабильно — на медленном CI падает, на быстром проходит; «починка» — увеличить задержку, снова нестабильно.
- Медленно — каждый
setTimeout— потеря сотен миллисекунд; сотня тестов = минуты. - Скрывает ошибки — асинхронная проблема «событие запоздало на 80 мс» проходит при задержке 500 мс, но в production клиент уже получил timeout.
Корректно — синхронный поток:
CLOCKчерез DI-токен → в тестеfixedClockс заданным значением.UUID_PROVIDERчерез DI-токен → в тесте фиксированный провайдер.- Outbox-relay вызывается явно в тесте, не ждём фонового
@Interval. - Kafka/Redis отключены конфигом — нет ожидания consumer-а.
export const CLOCK = Symbol('CLOCK');
export const UUID_PROVIDER = Symbol('UUID_PROVIDER');
export interface Clock {
now(): Date;
}
export interface UuidProvider {
generate(): string;
}
const fixedClock: Clock = { now: () => new Date('2026-05-26T10:00:00.000Z') };
const fixedUuid = '11111111-1111-1111-1111-111111111111';
const fixedUuidProvider: UuidProvider = { generate: () => fixedUuid };
moduleRef = await Test.createTestingModule({ imports: [AppModule] })
.overrideProvider(CLOCK).useValue(fixedClock)
.overrideProvider(UUID_PROVIDER).useValue(fixedUuidProvider)
.compile();
NODETEST-X2: прямые вызовы new Date() и randomUUID() в Handler/Service/Aggregate запрещены — делают тест недетерминированным.
Если логика строится на таймерах (setInterval, setTimeout внутри сервиса) — в тесте jest.useFakeTimers() + jest.advanceTimersByTime(ms), не реальное ожидание.
AAA-структура
NODETEST-3: один тест — один сценарий, три блока с пустой строкой между ними.
it('BR-ORDER-7: cancel order when CANCELLED — returns 409', async () => {
const orderId = '22222222-2222-2222-2222-222222222222';
// Arrange
await preparer
.createCustomer({ id: 'c1', email: 'customer@sber.ru' })
.createOrder({ id: orderId, status: 'CANCELLED', customerId: 'c1' })
.prepare();
// Act
const response = await request(app.getHttpServer())
.delete(`/v1/orders/${orderId}`)
.set('Authorization', `Bearer ${successToken()}`);
// Assert
expect(response.status).toBe(409);
expect(response.body.code).toBe('ORDER_ALREADY_CANCELLED');
});
Каждый блок — одна ответственность:
- Arrange — очистка БД (через
DatabasePreparer), создание фикстур, настройкаnock-стабов. - Act — один HTTP-вызов через supertest; не несколько, не «создать и сразу проверить».
- Assert — проверки ответа и побочных эффектов (строки в БД, Outbox-события).
Не «проверить весь жизненный цикл в одном тесте»
// ПЛОХО — мега-тест проверяет 5 переходов состояния
it('order lifecycle', async () => {
// создать заказ
// подтвердить
// оплатить
// отгрузить
// отменить
});
Что плохо:
- Если падает шаг 3 — неизвестно, что не так с шагами 4–5.
- Имя теста не описывает ни одного конкретного сценария.
- Нельзя запустить отдельный сценарий через
--testNamePattern.
Корректно — отдельные it:
it('BR-001: create order when valid — returns 201', async () => { ... });
it('BR-002: confirm order when DRAFT — returns 200', async () => { ... });
it('BR-003: pay order when CONFIRMED — returns 200', async () => { ... });
it('BR-ORDER-7: cancel order when CANCELLED — returns 409', async () => { ... });
Имена тестов
NODETEST-16: формат '<action> when <condition> — <expected>' внутри it(...).
it('create order when valid — returns 201', async () => { ... });
it('create order when customer not found — returns 404', async () => { ... });
it('confirm order when DRAFT — returns 200 and creates outbox event', async () => { ... });
it('confirm order when RESERVATION_FAILED — returns 409', async () => { ... });
it('get order when not found — returns 404', async () => { ... });
BR-код цитируется в начале имени теста — связывает его со спецификацией:
describe('POST /v1/orders/:id/confirm', () => {
it('BR-002: confirm order when DRAFT — returns 200', async () => { ... });
it('BR-007: confirm order when RESERVATION_FAILED — returns 409', async () => { ... });
});
describe-блок отражает endpoint или вариант использования; it — конкретный сценарий с BR-кодом.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
await new Promise(r => setTimeout(r, ...)) в тесте | NODETEST-X1 | синхронный flow, overrideProvider(CLOCK) |
Polling-цикл while (!result) | NODETEST-X1 | вызвать relay/worker явно |
new Date() в Handler/Service/Aggregate напрямую | NODETEST-X2 | токен CLOCK, override в TestingModule |
randomUUID() в Handler/Service/Aggregate напрямую | NODETEST-X2 | токен UUID_PROVIDER, override в TestingModule |
Один it на весь жизненный цикл заказа | NODETEST-3 | один it = один сценарий |
| Kafka-контейнер в базовом integration-тесте | NODETEST-X4 | Outbox-таблица через DatabasePreparer |
it без BR-кода в имени | NODETEST-16 | 'BR-NNN: ...' в начале имени |
Мок бизнес-логики (Handler/Aggregate) через jest.mock() в integration-тесте | NODETEST-X5 | мок только для внешней системы (nock/msw) |
Куда дальше
- TestingModule — платформенный setup —
globalSetup, контейнер, фабрика модуля. - DatabasePreparer — fluent setup БД:
clear*,create*,prepare(). - Fluent builders — билдеры сущностей с дефолтами.
- Один тест — структура, имена, supertest, successToken.
- Kafka, Redis, async — по умолчанию НЕТ — Outbox-подход.
- Пирамида тестов — что чем покрывать: unit агрегата, unit контроллера, интеграционный, E2E.