Testing InversifyJS Classes

Use @suites/di.inversify with Jest or Vitest to test an InversifyJS class.

Choose your test boundary: Suites reads injection metadata and constructs your class with controlled dependencies. Use an InversifyJS Container with the relevant production configuration to verify bindings, scopes, or activation hooks. 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 InversifyJS Container 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 and InversifyJS 7 or 8. Check Installation for supported runtime and runner versions.

Follow Installation with @suites/di.inversify and one doubles adapter. Include these references in your test TypeScript project:

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

For Vitest, change the first reference to @suites/doubles.vitest/unit. Load only one DI adapter’s metadata reference and one doubles reference per test project.

Enable experimentalDecorators and emitDecoratorMetadata, and configure your runner’s transformer to emit legacy decorator metadata. See runner configuration.

2. Define a token and its consumer

An injection token identifies a dependency when a TypeScript interface has no runtime class. Export it and use the same token in both the constructor and the test.

src/user.service.ts
import 'reflect-metadata';
import { inject, injectable } from 'inversify';
export const USER_DIRECTORY = Symbol('USER_DIRECTORY');
export interface UserDirectory {
findName(id: string): Promise<string>;
}
@injectable()
export class NameFormatter {
format(name: string): string {
return name.trim();
}
}
@injectable()
export class UserService {
constructor(
@inject(USER_DIRECTORY) private readonly directory: UserDirectory,
@inject(NameFormatter) private readonly formatter: NameFormatter,
) {}
async greet(id: string): Promise<string> {
const name = await this.directory.findName(id);
return `Hello, ${this.formatter.format(name)}!`;
}
}

Load reflect-metadata before decorated classes. You can also load it once in your runner’s setup file. No Container.bind() calls are needed for this test.

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, USER_DIRECTORY, UserService, type UserDirectory } from './user.service';
describe('UserService, solitary', () => {
let unit: UserService;
let directory: Mocked<UserDirectory>;
let formatter: Mocked<NameFormatter>;
beforeAll(async () => {
const testBed = await TestBed.solitary(UserService).compile();
unit = testBed.unit;
directory = testBed.unitRef.get<UserDirectory>(USER_DIRECTORY);
formatter = testBed.unitRef.get(NameFormatter);
});
it('greets a user from the directory', async () => {
directory.findName.mockResolvedValue(' Ada ');
formatter.format.mockReturnValue('Ada');
await expect(unit.greet('user-1')).resolves.toBe('Hello, Ada!');
expect(directory.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 the runner’s ESM configuration.

Run pnpm exec jest or pnpm exec vitest run. Expect one passing test: the generated mock supplies the name, and the service produces the greeting without a real directory implementation.

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 with selected real collaborators. Add this test to the same spec file, using its existing imports:

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

.expose(NameFormatter) selects the real class. The directory remains mocked. Select each real collaborator explicitly and check its dependencies for I/O. Exposed classes cannot be retrieved through unitRef.

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

Retrieve a named dependency

Supply the matching metadata, the name or tags attached to an injection site, when retrieving or configuring its mock.

src/directory.service.ts
import 'reflect-metadata';
import { inject, injectable, named } from 'inversify';
import { USER_DIRECTORY, type UserDirectory } from './user.service';
@injectable()
export class DirectoryService {
constructor(
@inject(USER_DIRECTORY) @named('primary') private readonly primary: UserDirectory,
) {}
findName(id: string): Promise<string> {
return this.primary.findName(id);
}
}
src/directory.service.spec.ts
import { beforeAll, describe, expect, it } from '@jest/globals';
import { TestBed, type Mocked } from '@suites/unit';
import { DirectoryService } from './directory.service';
import { USER_DIRECTORY, type UserDirectory } from './user.service';
describe('DirectoryService', () => {
let unit: DirectoryService;
let primary: Mocked<UserDirectory>;
beforeAll(async () => {
const testBed = await TestBed.solitary(DirectoryService).compile();
unit = testBed.unit;
primary = testBed.unitRef.get<UserDirectory>(USER_DIRECTORY, { named: 'primary' });
});
it('reads the named primary directory', async () => {
primary.findName.mockResolvedValue('Ada');
await expect(unit.findName('user-1')).resolves.toBe('Ada');
expect(primary.findName).toHaveBeenCalledWith('user-1');
});
});

To configure the named mock before compilation, replace the previous beforeAll hook with this one:

beforeAll(async () => {
const testBed = await TestBed.solitary(DirectoryService)
.mock<UserDirectory>(USER_DIRECTORY, { named: 'primary' })
.impl(stub => ({ findName: stub().mockResolvedValue('Ada') }))
.compile();
unit = testBed.unit;
primary = testBed.unitRef.get<UserDirectory>(USER_DIRECTORY, { named: 'primary' });
});

For @tagged('region', 'eu'), pass { region: 'eu' }. When an injection site has both a name and tags, include the complete record, such as { named: 'primary', region: 'eu' }. See Types.

Supply values and understand the boundary

NeedApproach
Configure a generated mock during the testunitRef.get<Dependency>(TOKEN)
Configure a mock before construction.mock<Dependency>(TOKEN).impl(stub => ({ ... }))
Inject an existing value or implementation.mock<Dependency>(TOKEN).final(value)
Exercise a selected real class.sociable(Service).expose(Collaborator)
Validate application bindings, scopes, or container modulesTest using your configured InversifyJS Container

Suites reads constructor and supported property injection metadata, including names and tags. It does not recreate production binding configuration, container hierarchies, scopes, or activation hooks. Direct imports and exposed code execute normally; Suites does not detect or block their I/O.

Token dependencies are mocked by default, but .final() can supply a value or implementation. Final values and exposed real classes are not retrievable through unitRef.

Summary

  • Share injection tokens between the application class and the test.
  • Retrieve token mocks with an explicit dependency type.
  • Match names and tags in both lookups and overrides.
  • Select real classes with .expose() and use an application container test for production wiring.

Next steps

Additional resources