nestjs-testing-expert
Writes NestJS Vitest tests — testing modules, provider mocks, service/controller specs, Supertest e2e. Use for any test touching a NestJS service, controller, guard, or endpoint.
Savant verdict: Pending analysis
Imported from the source hub; Savant's analysis is still running.
Live evaluation
Not evaluated yet. Workspaces can request a live evaluation.
Security assessment
Scan pending.
Structure
- No license declaredConfirm you may reuse this skill before importing it into your repository.
SKILL.md
---
name: nestjs-testing-expert
description: Writes NestJS Vitest tests — testing modules, provider mocks, service/controller specs, Supertest e2e. Use for any test touching a NestJS service, controller, guard, or endpoint.
metadata:
version: "2.2.3"
tags: "nestjs, testing, vitest, supertest, backend"
author: Ship Shit Dev
when_to_use: "Test.createTestingModule, override provider, supertest"
---
# NestJS 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.
```bash
bun add -D vitest unplugin-swc @swc/core @golevelup/ts-vitest supertest @types/supertest
```
```typescript
// 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:
```typescript
// 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' } })],
});
```
```json
{
"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:
```typescript
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.
```typescript
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.
```typescript
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: 'test@example.com' }];
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.
```typescript
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.
```typescript
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: 'test@example.com', name: 'Test' })
.expect(201);
const read = await request(app.getHttpServer())
.get(`/api/users/${created.body.id}`)
.expect(200);
expect(read.body.email).toBe('test@example.com');
});
});
```
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.
```typescript
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 `afterEach` so 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()` with
`vi.setSystemTime`) and pin the timezone rather than asserting on the real one.
- Restore spies with `vi.restoreAllMocks()` in `afterEach` when a spec uses
`vi.spyOn` on 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