Testing in Isolation (Solitary)

A solitary test is a unit test where all dependencies are mocked. Use it to check your class’s behavior with controlled responses from its collaborators.

Complete Installation for your framework and runner, or start with your framework quick start. Unit Testing Fundamentals and Test Doubles are optional background; you can follow this example without reading them first.

Step 1: Define the service and its dependencies

Choose your framework’s version of src/user.service.ts. Each version defines the same service, repository, and formatter, so the test below works unchanged with any of the three adapters.

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)}!`;
}
}
src/user.service.ts
import 'reflect-metadata';
import { injectable, inject } from 'inversify';
@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(
@inject(UserRepository) private readonly repository: UserRepository,
@inject(NameFormatter) private readonly formatter: NameFormatter,
) {}
async greet(id: string): Promise<string> {
const name = await this.repository.findName(id);
return `Hello, ${this.formatter.format(name)}!`;
}
}
src/user.service.ts
import 'reflect-metadata';
import { Injectable } from 'injection-js';
@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 test replaces it with a mock, an object whose methods are stubs that return the values you configure. No database is needed.

NestJS and InjectionJS read emitted constructor types. InversifyJS uses explicit @inject() decorators here. Keep the matching adapter’s /metadata declaration and decorator transform from Installation.

Step 2: Compile the test bed in beforeAll

Create this spec alongside the service. The example uses Jest; for Vitest, change the runner import from @jest/globals to vitest.

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 ');
});
});

unit is the class under test. unitRef.get() returns the generated dependency mock with the selected runner’s mock methods and TypeScript types.

The formatter is also mocked: its real trim() implementation does not run. The test explicitly supplies the formatted value.

Step 3: Run the test

Use your configured runner:

Terminal window
# Jest
pnpm exec jest
# Vitest
pnpm exec vitest run

Expect one passing test. The service produces the greeting from the configured dependency responses without executing the repository or formatter implementations.

Step 4: Configure different responses

A stub starts without a configured return value. Set every response that the tested behavior needs, including values used from collaborator methods.

To test a failure, replace the previous it block with this one:

it('propagates a repository error', async () => {
repository.findName.mockRejectedValue(new Error('User not found'));
await expect(unit.greet('user-1')).rejects.toThrow('User not found');
});

If you keep multiple scenarios in one describe, clear mock call histories in beforeEach using your runner’s clearAllMocks(). Set the required stub responses in each test; use resetAllMocks() when responses also need to be cleared. Keep compilation in beforeAll.

To configure a dependency before construction, use .mock(Dependency).impl(...) inside the setup hook. See Mock Configuration for complete examples and supplied values with .final().

Use injection tokens with the same workflow

The example uses class dependencies so its test is identical across frameworks. For a token dependency, retrieve the mock with unitRef.get<Dependency>(TOKEN) and configure its methods in the same way.

Summary

  • Compile the test bed in beforeAll and access the class through unit.
  • Retrieve dependency mocks through unitRef.
  • Set the responses each test needs and assert the result.
  • Use a solitary test when all injected collaborators should remain mocked.

Next steps