Testing InjectionJS Services

Use @suites/di.injectionjs with Jest or Vitest to test a service that uses constructor injection.

Choose your test boundary: Suites reads supported constructor metadata and creates your service with controlled dependencies. Use InjectionJS’s ReflectiveInjector with the relevant production configuration to verify provider factories, multi-providers, or hierarchy lookup. 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 ReflectiveInjector 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 your test project

This guide uses Suites 4.1 or later and InjectionJS ^2.6.1; CommonJS and ESM are supported. See Installation for Node.js and runner requirements.

Complete Installation with the InjectionJS adapter and your chosen runner. For an existing Jest project:

Terminal window
pnpm add -D @suites/unit @suites/di.injectionjs @suites/doubles.jest
pnpm add injection-js reflect-metadata

For Vitest, install @suites/doubles.vitest in place of @suites/doubles.jest.

Include this file in your test tsconfig:

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

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

Enable experimentalDecorators and emitDecoratorMetadata. Your test runner’s TypeScript transformer must emit decorator metadata too; use the runner configuration from Installation.

Vitest module identity

Add this property inside the existing test object in vitest.config.ts:

server: { deps: { external: ['injection-js'] } },

Keep your existing transformer and test settings. Externalizing injection-js makes the adapter and your tests use the same native module, preserving decorator and token identity.

2. Define a service and its token

An injection token identifies a dependency when a runtime class is not available, such as a TypeScript interface. Export the token so your service and tests share the same instance.

src/user.service.ts
import 'reflect-metadata';
import { Inject, Injectable, InjectionToken } from 'injection-js';
export interface UserRepository {
findName(id: string): Promise<string>;
}
export const USER_REPOSITORY = new InjectionToken<UserRepository>('UserRepository');
@Injectable()
export class UserService {
constructor(@Inject(USER_REPOSITORY) private readonly repository: UserRepository) {}
findName(id: string): Promise<string> {
return this.repository.findName(id);
}
}

Load reflect-metadata before decorated classes. You can also import it once in your runner’s setup file.

3. Test with mocked collaborators

A solitary test exercises the unit with all injected collaborators mocked. Compile the test bed in beforeAll, then configure the repository response and assert the result in it.

src/user.service.spec.ts
import { beforeAll, describe, expect, it } from '@jest/globals';
import { TestBed, type Mocked } from '@suites/unit';
import { USER_REPOSITORY, UserService } from './user.service';
import type { UserRepository } from './user.service';
describe('UserService, solitary', () => {
let unit: UserService;
let repository: Mocked<UserRepository>;
beforeAll(async () => {
const testBed = await TestBed.solitary(UserService).compile();
unit = testBed.unit;
repository = testBed.unitRef.get<UserRepository>(USER_REPOSITORY);
});
it('returns the repository result', async () => {
repository.findName.mockResolvedValue('Alice');
await expect(unit.findName('1')).resolves.toBe('Alice');
expect(repository.findName).toHaveBeenCalledWith('1');
});
});

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

Run pnpm exec jest or pnpm exec vitest run. Expect one passing test: the service returns Alice, and the generated repository mock receives '1'.

Your first test passes

You have a working solitary test. Continue to Solitary Unit Tests to extend this pattern, or Sociable Unit Tests to keep selected constructor-injected classes real. The remaining sections are optional details for dependency overrides and adapter support.

Override a dependency

To configure behavior before compilation, replace the previous beforeAll hook with this one. Keep the same imports and test:

beforeAll(async () => {
const testBed = await TestBed.solitary(UserService)
.mock<UserRepository>(USER_REPOSITORY)
.impl(stub => ({ findName: stub().mockResolvedValue('Alice') }))
.compile();
unit = testBed.unit;
repository = testBed.unitRef.get<UserRepository>(USER_REPOSITORY);
});

Use .final(...) to inject a supplied value instead. Final values cannot be retrieved through unitRef. Pass an explicit dependency type to token lookups and overrides, such as .mock<UserRepository>(USER_REPOSITORY).

Tokens are compared by identity. Another new InjectionToken<UserRepository>('UserRepository') is a different dependency even though its description matches.

Supported constructor patterns

PatternBehavior
Class dependenciesReads emitted constructor metadata
@Inject(token)Accepts classes, nonempty strings, symbols, and InjectionToken instances
@Inject(forwardRef(() => Dependency))Resolves the reference to the underlying token
Static parameters or lowered ctorParametersReads constructor metadata supported by InjectionJS
TestBed.sociable(...).expose(...)Keeps selected class dependencies real; those classes must also use supported constructor injection

Arbitrary object, numeric, and boolean tokens are not supported. Use InjectionToken when you need object identity.

Scope and limitations

The adapter supports constructor injection. Contextual inject() calls, including field initializers, are not supported. Use constructor injection for the class under test and any exposed real dependencies, or test those classes with InjectionJS’s own injector.

Suites builds a flat test container. It does not consume production provider configuration, run provider factories, collect multi-providers, or recreate injector hierarchies. @Optional(), @Self(), @SkipSelf(), and @Host() do not reproduce production lookup rules.

Optional dependencies are mocked like other dependencies. To test an absent optional dependency, supply null explicitly, for example .mock<UserRepository | null>(USER_REPOSITORY).final(null).

Summary

  • Configure the InjectionJS metadata reference and your runner’s decorator transform.
  • Share injection tokens between the service and its test.
  • Compile in beforeAll and retrieve typed mocks through unitRef.
  • Use constructor injection for the unit and any exposed real dependencies.

Next steps