NestJS / 38 минут
Тесты и подмена зависимостей
Собирайте TestingModule и заменяйте реальные зависимости контролируемыми doubles.
- Собирайте TestingModule и заменяйте реальные зависимости контролируемыми doubles.
- Применить тему в задаче «Тесты и подмена зависимостей» и объяснить граничные случаи.
Около 8 минут на чтение и разбор примеров. Практика — отдельно, в своём темпе.
Проверяемый контракт
Тест полезен, когда подтверждает наблюдаемое поведение: правильный результат, отказ, отсутствие приватных полей или согласованность записи. Проверка имени класса либо точной последовательности внутренних вызовов часто слишком тесно связана с реализацией. Сначала сформулируйте, что обещает сценарий пользователю и какой сбой должен обнаружить тест.
Unit-тест сервиса не требует открытого HTTP-порта. Передайте контролируемый репозиторий и вызовите метод напрямую. Интеграционный тест проверяет взаимодействие компонентов, например маршрута, ValidationPipe и guard. End-to-end тест собирает приложение с реальной инфраструктурой в отдельном окружении. Не называйте тест с подменённой БД проверкой настоящей транзакции.
const repo = { find: jest.fn().mockResolvedValue(null) };
const service = new BooksService(repo);
expect(await service.title('missing')).toBeNull();Пример предполагает конструктор BooksService с соответствующим контрактом. Такой тест проверяет отсутствие книги, но ничего не доказывает о DI-регистрации или HTTP 404. Именно поэтому нужны разные уровни, а не один огромный тест, пытающийся охватить каждую деталь одновременно.
TestingModule собирает контейнер
Пакет @nestjs/testing предоставляет Test.createTestingModule. compile строит модуль, разрешает зависимости и возвращает TestingModule. Его можно использовать без сервера для получения сервиса по токену. Если нужен HTTP, создайте приложение, примените настройки bootstrap и вызовите init.
const moduleRef = await Test.createTestingModule({
providers: [{ provide: 'SOURCE', useValue: 'real' }],
}).compile();
try {
expect(moduleRef.get('SOURCE')).toBe('real');
} finally {
await moduleRef.close();
}Не забывайте закрывать модуль: его провайдеры могут владеть соединениями и таймерами. Тест, который проходит только при принудительном завершении процесса, способен скрывать утечку ресурса. Ошибка до конца проверки тоже должна приводить к освобождению через finally или надёжный afterAll.
Подмена до compile
overrideProvider заменяет зависимость в тестовой сборке, не меняя production-класс. Подмена выполняется до compile, пока контейнер ещё не создал граф. useValue удобен для точных результатов, useClass — для отдельного fake-адаптера. Fake должен сохранять важный контракт, включая ошибки и асинхронность.
const moduleRef = await Test.createTestingModule({ imports: [BooksModule] })
.overrideProvider(BOOKS)
.useValue({ find: async () => ({ id: '1', title: 'JS' }) })
.compile();BOOKS должен быть тем же токеном, который использует модуль. Созданный рядом новый Symbol с тем же описанием не подменит провайдер. Не исправляйте тест экспортом приватных внутренних полей сервиса: лучше получить зависимый сервис через контейнер и проверить результат публичного метода.
Настоящий HTTP без постоянного порта
Supertest может отправлять запросы к серверу, полученному через app.getHttpServer. Вызывайте app.init перед запросами. Production-приложение может иметь global prefix, ValidationPipe, cookie parser и filters; тестовая сборка должна применять тот же общий configureApp. Иначе HTTP-тест проверит другое приложение с более слабыми границами.
const app = moduleRef.createNestApplication();
try {
configureApp(app);
await app.init();
await request(app.getHttpServer()).get('/api/books/missing').expect(404);
} finally {
await app.close();
}configureApp здесь — функция проекта. Не требуется одновременно вызывать постоянный listen с фиксированным портом. Проверяйте JSON и заголовки, а не только статус. Успех 200 не доказывает, что passwordHash отсутствует, cookies безопасно настроены или форма ответа соответствует обещанию.
Ошибки, конкуренция и данные
Для каждого важного сценария полезны обычный вход, граница и отказ зависимости. В доступе к книге проверьте чужого владельца и отсутствие сессии. В DTO — лишние поля, null и неверный тип. В наградах — повторный и конкурентный запрос, чтобы клиент не мог получить XP дважды. Изоляция аккаунтов должна проверяться двумя отдельными пользователями.
Тесты БД используют отдельную test-базу с явной защитой имени окружения. Примените миграции и создавайте данные с уникальным префиксом; очищайте только принадлежащие тесту записи. Не используйте production credentials в CI. Подменённый runner позволяет проверить сравнение результатов API, но изоляцию пользовательского кода проверяют отдельные Docker-тесты.
Практика overrideProvider
Создайте TestingModule с TOKEN, изначально равным real. До compile замените TOKEN переданным text через overrideProvider и useValue. Получите значение из модуля и верните его из solve. Закройте модуль даже при ошибке. Пустая строка должна остаться пустой строкой, поскольку она допустима по контракту.
После решения подмените репозиторий книги и проверьте отсутствие записи и отказ хранилища. Затем добавьте один HTTP-тест с настоящим ValidationPipe. Объясните, какое поведение подтверждает каждый уровень и какие свойства остаются за пределами проверки. Честные границы теста полезнее утверждения, что один зелёный запуск гарантирует полную корректность приложения.