Testing NestJS Applications
Test NestJS applications with Vitest: unit test services with mocked repositories, test controllers with the Test module, and write end-to-end tests with Supertest.
On this page 11 sections
Introduction
Automated tests let you change code with confidence: if a refactor breaks the login flow, a test tells you in seconds instead of a user telling you in production. NestJS was designed with testing in mind — dependency injection makes it easy to swap real dependencies for fakes, and every new project ships with a working test setup.
- The testing tools Nest sets up for you.
- How Test.createTestingModule() builds an isolated DI container.
- How to unit test a service with a mocked repository.
- How to unit test a controller with a mocked service.
- How to write end-to-end tests against real HTTP endpoints.
Testing Setup in Nest
| Command | Runs |
|---|---|
| npm run test | All unit tests (*.spec.ts files in src/). |
| npm run test:watch | Unit tests, re-running on every change. |
| npm run test:cov | Unit tests with a code coverage report. |
| npm run test:e2e | End-to-end tests (*.e2e-spec.ts files in test/). |
Vitest is the test runner and assertion library; Supertest sends real HTTP requests in e2e tests; and @nestjs/testing provides the Test class used to build modules for tests. All three are installed by nest new, and Vitest is configured with globals enabled, so describe, it, expect and vi are available in every test file without importing them.
NestJS 12 projects use Vitest. Projects created with NestJS 10 or 11 use Jest instead. The two share almost the same API — describe, it, expect, beforeEach and matchers like toEqual are identical — the main difference is the mock helper: vi.fn() and vi.clearAllMocks() in Vitest, jest.fn() and jest.clearAllMocks() in Jest. Everything else in this lesson applies to both.
The Testing Module
Test.createTestingModule() takes the same metadata as @Module() and builds a small, isolated DI container just for the test. The key benefit: you can replace any provider with a mock.
const moduleRef = await Test.createTestingModule({ providers: [ ProductsService, { provide: getRepositoryToken(Product), useValue: mockRepository }, ],}).compile();
const service = moduleRef.get(ProductsService);getRepositoryToken(Product) (from @nestjs/typeorm) returns the exact token that @InjectRepository(Product) asks for, so the service receives your mock instead of a real database repository.
Unit Testing a Service
// src/products/products.service.spec.tsimport { NotFoundException } from '@nestjs/common';import { Test } from '@nestjs/testing';import { getRepositoryToken } from '@nestjs/typeorm';import { Product } from './entities/product.entity.js';import { ProductsService } from './products.service.js';
describe('ProductsService', () => { let service: ProductsService; const mockRepository = { findOneBy: vi.fn(), find: vi.fn(), create: vi.fn(), save: vi.fn(), };
beforeEach(async () => { vi.clearAllMocks(); const moduleRef = await Test.createTestingModule({ providers: [ ProductsService, { provide: getRepositoryToken(Product), useValue: mockRepository }, ], }).compile();
service = moduleRef.get(ProductsService); });
it('returns a product when it exists', async () => { const laptop = { id: 1, name: 'Laptop', price: 55000 }; mockRepository.findOneBy.mockResolvedValue(laptop);
await expect(service.findOne(1)).resolves.toEqual(laptop); expect(mockRepository.findOneBy).toHaveBeenCalledWith({ id: 1 }); });
it('throws NotFoundException when the product is missing', async () => { mockRepository.findOneBy.mockResolvedValue(null);
await expect(service.findOne(99)).rejects.toThrow(NotFoundException); });});Click Run to see what this code prints.
Unit Testing a Controller
A controller test checks that the controller passes the right data to its service and returns the result. The service itself is mocked:
// src/products/products.controller.spec.tsdescribe('ProductsController', () => { let controller: ProductsController; const mockService = { findAll: vi.fn(), create: vi.fn() };
beforeEach(async () => { const moduleRef = await Test.createTestingModule({ controllers: [ProductsController], providers: [{ provide: ProductsService, useValue: mockService }], }).compile();
controller = moduleRef.get(ProductsController); });
it('creates a product through the service', async () => { const dto = { name: 'Phone', price: 20000 }; mockService.create.mockResolvedValue({ id: 2, ...dto });
await expect(controller.create(dto)).resolves.toEqual({ id: 2, ...dto }); expect(mockService.create).toHaveBeenCalledWith(dto); });});End-to-End Testing
End-to-end (e2e) tests start the real Nest application and send real HTTP requests, checking the whole pipeline — routing, validation pipes, guards, filters and serialization — together. You can still override specific providers, such as the database layer:
// test/products.e2e-spec.tsimport { INestApplication, ValidationPipe } from '@nestjs/common';import { Test } from '@nestjs/testing';import request from 'supertest';import { AppModule } from '../src/app.module.js';import { ProductsService } from '../src/products/products.service.js';
describe('Products (e2e)', () => { let app: INestApplication; const productsService = { findAll: () => [{ id: 1, name: 'Laptop' }] };
beforeAll(async () => { const moduleRef = await Test.createTestingModule({ imports: [AppModule] }) .overrideProvider(ProductsService) .useValue(productsService) .compile();
app = moduleRef.createNestApplication(); app.useGlobalPipes(new ValidationPipe({ whitelist: true })); await app.init(); });
it('GET /products returns the product list', () => { return request(app.getHttpServer()) .get('/products') .expect(200) .expect([{ id: 1, name: 'Laptop' }]); });
it('POST /products rejects invalid data', () => { return request(app.getHttpServer()) .post('/products') .send({ name: '' }) .expect(400); });
afterAll(async () => { await app.close(); });});Anything you configure in main.ts (global pipes, prefixes, filters) does not run in tests, because tests build the app with createNestApplication() instead of calling bootstrap(). Apply the same setup in the test, or move it into a shared function both use. Registering global pipes/guards with APP_PIPE/APP_GUARD providers avoids this entirely.
NestJS 12 projects use import request from 'supertest'. Some older projects (without esModuleInterop) use import * as request from 'supertest' instead. Match whatever your generated app.e2e-spec.ts file already does.
Common Mistakes
- Letting unit tests hit a real database — they become slow and flaky. Mock the repository instead.
- Forgetting to reset mocks between tests, so one test's setup leaks into the next.
- Forgetting await app.close() in e2e tests, leaving open servers and database connections behind.
- Testing only the "happy path" and never the error cases.
Best Practices
- Unit test services thoroughly — that is where business logic lives.
- Keep controller tests light, focused on wiring between HTTP and services.
- Use e2e tests for critical user flows such as registration, login and checkout.
- Run tests automatically in CI on every push.
- Use a separate test database (or a disposable container) for e2e tests that need real persistence.
Frequently Asked Questions
Unit tests check one class in isolation with its dependencies mocked — fast and precise. E2E tests run the whole app through real HTTP requests — slower, but they prove the pieces work together.
There is no magic number. Prioritize meaningful tests of business logic and critical flows over chasing a coverage percentage.
Key Takeaways
- Nest 12 ships with Vitest, Supertest and @nestjs/testing preconfigured (older versions use Jest).
- Test.createTestingModule() builds an isolated DI container for each test.
- Custom providers (useValue) and getRepositoryToken() replace real dependencies with mocks.
- overrideProvider() swaps providers inside a full AppModule for e2e tests.
- E2E tests use createNestApplication() and Supertest to send real HTTP requests.
Summary
You can now prove your API works — and keep proving it as it changes. In the final lesson, you will bring everything together with production best practices: API documentation, security hardening, logging and deployment.