Testing NestJS Services

Use @suites/di.nestjs with Jest or Vitest to test a NestJS provider, a class managed by dependency injection.

Choose your test boundary: Suites reads injection metadata and constructs your service with controlled dependencies. Use Nest’s Test.createTestingModule() with the relevant production modules and providers to verify their wiring and resolution. Both approaches can use mocks. Compare Suites with framework test containers.

Framework IoC container and Suites TestBed Two construction paths for the same UserService. The framework container uses registered providers and its own factories and resolution rules. Its test configuration can register real dependencies or mocks. Suites reads injection metadata, creates dependency mocks, and constructs UserService. Sociable tests can expose selected real classes. Suites does not load the framework's provider registrations. Both approaches can use mocks. Framework IoC container Registrations Modules / bindings / providers NestJS TestingModule Factories + resolution rules UserService real class Real dependency Mock provider Exercises the registrations included in this test. Suites TestBed Injection metadata Constructor types + tokens Suites TestBed Generate mocks + construct UserService real class Auto mock Exposed class Mocks by default; selected real classes with sociable().expose().
Both approaches can use mocks. Native containers resolve test registrations; Suites builds the class from injection metadata. Solid dependency boxes are real; dashed boxes are mocks.

1. Configure the test project

This guide targets Suites 4. Check Installation for supported NestJS and runner versions.

Follow Installation with @suites/di.nestjs and either @suites/doubles.jest or @suites/doubles.vitest. Include these references in the TypeScript project that runs your tests:

suites.d.ts
/// <reference types="@suites/doubles.jest/unit" />
/// <reference types="@suites/di.nestjs/metadata" />

For Vitest, change the first reference to @suites/doubles.vitest/unit. Keep exactly one doubles reference and one DI reference per test project.

Enable experimentalDecorators and emitDecoratorMetadata. Your test runner’s transformer must emit legacy decorator metadata too. Use the runner configuration from Installation; the TypeScript options alone do not configure the transform.

2. Define the service and its collaborators

src/user.service.ts
import 'reflect-metadata';
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserRepository {
async findName(id: string): Promise<string> {
throw new Error(`Database access is not configured for ${id}`);
}
}
@Injectable()
export class NameFormatter {
format(name: string): string {
return name.trim();
}
}
@Injectable()
export class UserService {
constructor(
private readonly repository: UserRepository,
private readonly formatter: NameFormatter,
) {}
async greet(id: string): Promise<string> {
const name = await this.repository.findName(id);
return `Hello, ${this.formatter.format(name)}!`;
}
}

The repository deliberately throws if its real method runs. The following tests replace it, so no database implementation is needed. Load reflect-metadata before decorated classes, either as shown or in your runner’s setup file.

3. Test with mocked collaborators

A solitary test exercises the unit with all injected collaborators mocked.

src/user.service.spec.ts
import { beforeAll, describe, expect, it } from '@jest/globals';
import { TestBed, type Mocked } from '@suites/unit';
import { NameFormatter, UserRepository, UserService } from './user.service';
describe('UserService, solitary', () => {
let unit: UserService;
let repository: Mocked<UserRepository>;
let formatter: Mocked<NameFormatter>;
beforeAll(async () => {
const testBed = await TestBed.solitary(UserService).compile();
unit = testBed.unit;
repository = testBed.unitRef.get(UserRepository);
formatter = testBed.unitRef.get(NameFormatter);
});
it('greets the user returned by the repository', async () => {
repository.findName.mockResolvedValue(' Ada ');
formatter.format.mockReturnValue('Ada');
await expect(unit.greet('user-1')).resolves.toBe('Hello, Ada!');
expect(repository.findName).toHaveBeenCalledWith('user-1');
expect(formatter.format).toHaveBeenCalledWith(' Ada ');
});
});

For Vitest, import beforeAll, describe, expect, and it from vitest. For NodeNext ESM projects, use .js extensions for relative imports and your runner’s ESM configuration.

Run pnpm exec jest or pnpm exec vitest run. Expect one passing test: the repository receives the requested ID, and the service returns the greeting using the configured formatter result.

Your first test passes

You have a working solitary test. Continue to Solitary Unit Tests to extend this pattern, or Sociable Unit Tests to test selected real collaborators. The remaining examples are optional; use them when your service needs real collaborators, custom tokens, or framework-specific configuration.

4. Keep selected business logic real

A sociable test exercises the unit together with selected real collaborators. Add this test to the same spec file, using the imports above:

describe('UserService, sociable', () => {
let unit: UserService;
let repository: Mocked<UserRepository>;
beforeAll(async () => {
const testBed = await TestBed.sociable(UserService)
.expose(NameFormatter)
.compile();
unit = testBed.unit;
repository = testBed.unitRef.get(UserRepository);
});
it('formats the mocked repository response with the real formatter', async () => {
repository.findName.mockResolvedValue(' Ada ');
await expect(unit.greet('user-1')).resolves.toBe('Hello, Ada!');
});
});

NameFormatter.format() now runs its real trimming logic. The repository stays mocked. Exposed real classes cannot be retrieved through unitRef; see UnitReference.

Run the same test command again. With both examples in the spec file, expect two passing tests.

Supply custom injection tokens

Use @Inject(token) when a dependency has no runtime class, such as a configuration interface. Export the token so the service and test share its identity.

src/greeting.service.ts
import 'reflect-metadata';
import { Inject, Injectable } from '@nestjs/common';
export const GREETING_OPTIONS = Symbol('GREETING_OPTIONS');
export interface GreetingOptions {
prefix: string;
}
@Injectable()
export class GreetingService {
constructor(@Inject(GREETING_OPTIONS) private readonly options: GreetingOptions) {}
greet(name: string): string {
return `${this.options.prefix}, ${name}!`;
}
}
src/greeting.service.spec.ts
import { beforeAll, describe, expect, it } from '@jest/globals';
import { TestBed, type Mocked } from '@suites/unit';
import { GREETING_OPTIONS, GreetingService, type GreetingOptions } from './greeting.service';
describe('GreetingService', () => {
let unit: GreetingService;
beforeAll(async () => {
const testBed = await TestBed.solitary(GreetingService)
.mock<GreetingOptions>(GREETING_OPTIONS)
.final({ prefix: 'Welcome' })
.compile();
unit = testBed.unit;
});
it('uses the supplied greeting options', () => {
expect(unit.greet('Ada')).toBe('Welcome, Ada!');
});
});

.final() injects the supplied value as-is. It is not retrievable through unitRef. For token-based method mocks, use unitRef.get<DependencyType>(TOKEN) or configure .mock<DependencyType>(TOKEN).impl(...). NestJS calls do not take a second metadata argument.

Choose the right test boundary

Test goalApproach
Service behavior with controlled collaboratorsTestBed.solitary(Service)
Service behavior with selected real classesTestBed.sociable(Service).expose(Collaborator)
Module imports, provider factories, or provider overridesNest’s Test.createTestingModule()
HTTP routing, guards, pipes, or interceptors in the request lifecycleA Nest application test with the relevant modules

Suites reads constructor and supported property injection metadata. It does not boot the Nest module graph, execute module provider factories, or validate your production provider registrations. Direct imports and exposed code execute normally, including any I/O they perform.

Summary

  • Load the NestJS adapter’s /metadata reference and configure decorator metadata emission.
  • Retrieve generated mocks by their dependency class or token.
  • Use .expose() for selected real collaborators and .final() for supplied values.
  • Use Nest’s testing tools when the test needs module wiring or application behavior.

Next steps

Additional resources

  • NestJS Suites recipe: Nest’s introduction to Suites. Use this guide’s /metadata declaration path and the current Jest or Vitest setup for Suites 4.