Photo by PJH on Unsplash

On this page

This chapter follows Authorization in NestJS.

Because no class in a Nest app creates its own dependencies, a test can decide what every dependency is. @nestjs/testing builds a small module graph containing only what the test needs, with fakes in place of databases and external APIs, and resolves it exactly as the application would. Projects created with the Nest CLI already have Jest and supertest set up. The same tests run under Vitest with minor changes. For Jest itself, see Testing TypeScript with Jest.

The testing module

Test.createTestingModule() takes the same metadata as @Module(). compile() builds it, and get() returns any provider from it:

src/cats/cats.controller.spec.ts
import { Test } from '@nestjs/testing';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
describe('CatsController', () => {
let controller: CatsController;
const catsService = { findAll: jest.fn(), findOne: jest.fn() };
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
controllers: [CatsController],
providers: [{ provide: CatsService, useValue: catsService }],
}).compile();
controller = moduleRef.get(CatsController);
});
it('returns the cats from the service', async () => {
catsService.findAll.mockResolvedValue([{ name: 'Tom' }]);
await expect(controller.findAll()).resolves.toEqual([{ name: 'Tom' }]);
});
});

The useValue provider from Providers and dependency injection is the whole trick: the controller asks for CatsService and receives the mock. A missing provider fails compile() with the same “can’t resolve dependencies” error as the real app, which is a quick way to see what a class depends on.

Unit tests for services

A service is usually tested with its own collaborators faked. Often the simplest test does not need Nest at all:

src/auth/auth.service.spec.ts
import * as argon2 from 'argon2';
import { AuthService } from './auth.service';
it('rejects a wrong password', async () => {
const users = { findByEmail: jest.fn().mockResolvedValue({ passwordHash: await argon2.hash('right') }) };
const auth = new AuthService(users as never, {} as never);
await expect(auth.validateUser('tom@example.com', 'wrong')).resolves.toBeNull();
});

Constructing the class directly is fine for a provider with one or two dependencies. Use the testing module once dependencies are injected by token, such as a Mongoose model under getModelToken(Cat.name), or when the test should prove that the module wiring is correct.

Overriding parts of a real module

A test can also start from a real module and replace only what it must. overrideProvider(), overrideGuard(), overrideInterceptor(), overridePipe(), and overrideFilter() swap one piece before compilation:

test/cats.e2e-spec.ts
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(CatsService)
.useValue(catsService)
.overrideGuard(ApiKeyGuard)
.useValue({ canActivate: () => true })
.compile();

overrideGuard() replaces guards bound with @UseGuards(). A global guard registered with { provide: APP_GUARD, useClass: JwtAuthGuard } cannot be reached that way, because the guard is not a provider of its own. Register it as { provide: APP_GUARD, useExisting: JwtAuthGuard } next to a plain JwtAuthGuard provider, and the test can call .overrideProvider(JwtAuthGuard). Faking authentication is convenient, but a few tests should still go through the real guard with a real token, so that the protection itself is tested.

End-to-end tests

An end-to-end test starts the whole application in memory and sends HTTP requests to it with supertest. Nothing listens on a port:

test/cats.e2e-spec.ts
import { INestApplication } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Test } from '@nestjs/testing';
import request from 'supertest';
import { AppModule } from '../src/app.module';
describe('Cats (e2e)', () => {
let app: INestApplication;
let token: string;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({ imports: [AppModule] }).compile();
app = moduleRef.createNestApplication();
await app.init();
token = app.get(JwtService).sign({ sub: 'user-1', email: 'tom@example.com', roles: ['user'] });
});
afterAll(() => app.close());
it('rejects an invalid cat', () =>
request(app.getHttpServer())
.post('/cats')
.set('Authorization', `Bearer ${token}`)
.send({ name: 'Tom', age: -1 })
.expect(400));
});

app.init() runs the lifecycle hooks, and app.close() runs the shutdown hooks, which closes database connections so that Jest can exit. The database should be a test instance, for example mongodb-memory-server, configured through the same environment variables the app already reads.

What main.ts does not give a test

The test builds the app from AppModule, not from main.ts. Anything main.ts adds to the application object is missing in the test: app.useGlobalPipes(), app.setGlobalPrefix(), app.enableCors(), app.use(helmet()). The validation test above would pass a negative age straight through if ValidationPipe were only registered in main.ts.

There are two fixes. Register global components as providers (APP_PIPE, APP_GUARD, APP_INTERCEPTOR, APP_FILTER) so they are part of the module graph, as recommended in Pipes and validation in NestJS. For settings that only exist on the application object, move them into a configureApp(app) function that both main.ts and the test setup call.

Running the tests

The CLI template defines the scripts:

Terminal window
pnpm test
pnpm test:watch
pnpm test:cov
pnpm test:e2e

Unit tests sit next to the code as *.spec.ts. End-to-end tests live in test/ as *.e2e-spec.ts with their own Jest configuration, test/jest-e2e.json. Keep the two apart: unit tests should run in seconds on every save, while end-to-end tests start the app and a database and belong in CI and before a merge.

Next: NestJS glossary.

Check your understanding

Question 1 of 6

0 of 6 checked

  1. 1. How does the controller test give CatsController a mock service?

Search articles

Type a word to search published articles.

Open the search page