Catalogskills.sh · shipshitdev

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.

Pending analysisVerified hub746 installs

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