About nestjs-testing-expert
nestjs-testing-expert is a skill for building Vitest tests for NestJS modules, services, controllers, and HTTP endpoints. It covers unit, integration, and end-to-end testing, with guidance on dependency injection, provider overrides, and database choices.
Key features
- 01Configure Vitest with SWC for NestJS metadata
- 02Test services and controllers with explicit collaborators
- 03Override providers and guards while preserving module wiring
- 04Exercise HTTP flows against a booted Nest application
- 05Choose database approaches for unit, integration, and e2e tests
- 06Manage teardown, state resets, and deterministic tests
Use cases
- 01Set up Vitest for a NestJS project
- 02Test provider wiring while faking external systems
- 03Verify authenticated HTTP endpoints end to end
- 04Reduce flaky or slow test suites
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ shipshitdev/ skills --skill nestjs-testing-expertAfter installing, type/nestjs-testing-expertin Claude Code to confirm it loaded.
Bundled filesplugin.json
Details
From the project READMENestJS Testing Expert
Build reliable Vitest suites for NestJS modules, services, controllers, and HTTP endpoints.
Scope
This skill owns NestJS mechanics: testing modules, provider wiring, and
request-level end-to-end flows. For framework-agnostic questions — which level a
behavior belongs at, what a coverage number means, how to design a test that
survives refactoring, how to kill a flake — use testing-expert.
When to Use
- Writing unit, integration, or end-to-end tests for a NestJS application
- Setting up a testing module, overriding providers, or faking a database
- Testing controllers, guards, interceptors, or pipes
- Exercising HTTP endpoints against a booted Nest application
Levels in a Nest Application
- Unit — a service or provider in isolation, collaborators replaced with test doubles. No module graph beyond the providers under test.
- Integration — a module compiled with its real providers, external systems faked. Proves the wiring and the contracts between layers.
- End-to-end — a booted application driven over HTTP. Proves the full request path: pipes, guards, controller, service, persistence.
Vitest Setup for NestJS
Nest's dependency injection reads decorator metadata (emitDecoratorMetadata).
Vitest transforms with esbuild, which does not emit it, so constructor
injection silently resolves to undefined. Compile with unplugin-swc, which
does emit decorator metadata.
bun add -D vitest unplugin-swc @swc/core @golevelup/ts-vitest supertest @types/supertest
// vitest.config.ts — unit and integration specs
import swc from 'unplugin-swc';
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
root: './',
environment: 'node',
include: ['src/**/*.spec.ts'],
testTimeout: 10_000,
},
plugins: [
swc.vite({
module: { type: 'es6' },
}),
],
});
Keep end-to-end specs in a separate config so bun run test stays fast and
bun run test:e2e boots the application:
// vitest.config.e2e.ts
import swc from 'unplugin-swc';
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
root: './',
environment: 'node',
include: ['test/**/*.e2e-spec.ts'],
testTimeout: 30_000,
},
plugins: [swc.vite({ module: { type: 'es6' } })],
});
{
"scripts": {
"test": "vitest run",
"test:e2e": "vitest run --config vitest.config.e2e.ts",
"test:cov": "vitest run --coverage"
}
}
Options from older runner configs map to Vitest one to one: path aliases move from
moduleNameMapper to resolve.alias (or the vite-tsconfig-paths plugin),
testEnvironment becomes test.environment, transform is replaced by the SWC
plugin, and per-suite timeouts use test.testTimeout or vi.setConfig({ testTimeout }).
Import describe, it, expect, vi, and the lifecycle hooks from vitest
explicitly when globals is off. Browser-driven end-to-end flows belong to
Playwright; Supertest here covers HTTP against a booted Nest app.
For auto-mocking whole providers, createMock from @golevelup/ts-vitest
returns a deep mock typed to the class:
import { createMock, DeepMocked } from '@golevelup/ts-vitest';
Service Specs
Compile a testing module with the subject real and every collaborator supplied explicitly. Injection tokens come from whatever integration provides them — an ORM's token helper, a class reference, or a custom token constant.
import { Test, TestingModule } from '@nestjs/testing';
import { afterEach, beforeEach, describe, expect, it, vi, type Mocked } from 'vitest';
describe('UsersService', () => {
let service: UsersService;
let repository: Mocked<UsersRepository>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{ provide: UsersRepository, useValue: createUsersRepositoryMock() },
],
}).compile();
service = module.get(UsersService);
repository = module.get(UsersRepository);
});
afterEach(() => {
vi.resetAllMocks();
});
it('returns only the active users of the requested organization', async () => {
const expected = [{ id: '1', organization: 'org1' }];
repository.find.mockResolvedValue(expected);
const result = await service.findAll('org1');
expect(result).toEqual(expected);
expect(repository.find).toHaveBeenCalledWith({
organization: 'org1',
isDeleted: false,
});
});
});
Assert on the arguments the collaborator received when they encode a business
rule — the isDeleted: false filter above is the behavior, not an implementation
detail.
Controller Specs
Register the controller, stub its service, and verify only the controller's own job: argument extraction, delegation, and response shaping. Business rules belong to the service spec.
describe('UsersController', () => {
let controller: UsersController;
let service: Mocked<UsersService>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [{ provide: UsersService, useValue: createUsersServiceMock() }],
}).compile();
controller = module.get(UsersController);
service = module.get(UsersService);
});
it('passes the organization from the request through to the service', async () => {
const expected = [{ id: '1', email: '[email protected]' }];
service.findAll.mockResolvedValue(expected);
const result = await controller.findAll('org1');
expect(result).toEqual(expected);
expect(service.findAll).toHaveBeenCalledWith('org1');
});
});
Overriding Providers
Compile the real module and swap only what must not run for real.
overrideProvider and overrideGuard keep the rest of the graph authentic, so
the test still proves the wiring.
const module: TestingModule = await Test.createTestingModule({
imports: [UsersModule],
})
.overrideProvider(MailerService)
.useValue(mailerMock)
.overrideGuard(AuthGuard)
.useValue({ canActivate: () => true })
.compile();
Override the guard only in tests about something else. Authorization itself deserves end-to-end tests that run the real guard.
Integration Tests
Boot the application, keep persistence real against a disposable database, and reset state between tests.
describe('Users (integration)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
beforeEach(async () => {
await resetDatabase();
});
it('persists a created user and returns it on read', async () => {
const created = await request(app.getHttpServer())
.post('/api/users')
.send({ email: '[email protected]', name: 'Test' })
.expect(201);
const read = await request(app.getHttpServer())
.get(`/api/users/${created.body.id}`)
.expect(200);
expect(read.body.email).toBe('[email protected]');
});
});
Close the application in afterAll. A leaked Nest application holds its
connection pool open and hangs the Vitest run.
End-to-End Tests
Same boot, driven as a real client — authentication included.
describe('Users API (e2e)', () => {
let app: INestApplication;
let authToken: string;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
authToken = await signInTestUser(app);
});
afterAll(async () => {
await app.close();
});
it('rejects an unauthenticated list request', () =>
request(app.getHttpServer()).get('/api/users').expect(401));
it('returns the caller organization users when authenticated', () =>
request(app.getHttpServer())
.get('/api/users')
.set('Authorization', `Bearer ${authToken}`)
.expect(200)
.expect((res) => {
expect(Array.isArray(res.body)).toBe(true);
}));
});
A shared signInTestUser helper keeps the token flow in one place and out of
every spec.
Database Strategy
| Approach | Fits | Cost |
|---|---|---|
| Repository test double | Unit specs | Fastest; proves no SQL or schema |
| In-memory or embedded engine | Integration specs | Fast; behavior can drift from production |
| Disposable container per run | Integration and e2e specs | Slowest; highest fidelity |
Reset between tests by truncating or rolling back a transaction rather than recreating the schema — schema rebuilds dominate suite runtime.
Tips
- Give each spec its own module compilation; a shared one leaks state between tests.
- Reset mocks in
afterEachso a stub set in one test cannot satisfy the next. - Boot the application once per describe block and reset data per test — booting per test is the usual cause of a slow Nest suite.
- Keep tests deterministic: freeze the clock (
vi.useFakeTimers()withvi.setSystemTime) and pin the timezone rather than asserting on the real one. - Restore spies with
vi.restoreAllMocks()inafterEachwhen a spec usesvi.spyOnon shared objects.
Checklist
- Clear arrange/act/assert structure
- Subject real, collaborators explicit — never the reverse
- Error and unauthorized paths covered, not just the happy path
- Application closed and mocks reset in teardown
- Fast to run