Suites
Unit testing in dependency injection systems often involves repetitive setup and brittle mock wiring. Suites is a unit testing framework that creates isolated test environments with a single declarative call, helping developers focus on test logic rather than infrastructure, boilerplate and setup.
When to use it
Use Suites when you:
- Need fast, maintainable unit tests for InversifyJS classes
- Want compile-time type safety for all mocked dependencies
- Need to eliminate repetitive mock setup boilerplate
- Want to test classes with both isolation and selective collaboration
- Need AI-assisted testing with clear, structured patterns
If you need full integration tests with real dependencies and actual InversifyJS containers, traditional container configuration may be more appropriate.
Core features
- Auto-generates type-safe mocks - All dependencies become fully typed
Mocked<T>instances with compile-time validation - Solitary and sociable testing - Test classes in complete isolation or enable selective collaboration between business logic
- InversifyJS-aware - Recognizes
@injectable,@inject,@multiInject,@tagged, and@nameddecorators automatically - Virtual DI container - No actual InversifyJS container needed - analyzes classes and creates only required mocks
- Token auto-mocking - Token-injected dependencies are always mocked, ensuring tests remain unit tests
- Framework integration - Works with Jest, Vitest, and Sinon out of the box
Installation
For InversifyJS 6.x:
npm install -D @suites/unit @suites/di.inversify@^3.0.0 @suites/doubles.jest
Use @suites/doubles.vitest or @suites/doubles.sinon if you're using Vitest or Sinon instead of Jest.
Add type references to global.d.ts:
/// <reference types="@suites/doubles.jest/unit" />
/// <reference types="@suites/di.inversify" />
Quick Examples
Solitary Testing
Test a class in complete isolation with all dependencies automatically mocked:
@injectable()
class OrderService {
constructor(
private readonly paymentGateway: PaymentGateway,
private readonly inventoryService: InventoryService
) {}
async processOrder(orderId: string) {
const inventory = await this.inventoryService.checkStock(orderId);
return this.paymentGateway.charge(inventory.total);
}
}
import { TestBed, type Mocked } from '@suites/unit';
import { injectable } from 'inversify';
describe('Order Service Unit Spec', () => {
let orderService: OrderService;
let paymentGateway: Mocked<PaymentGateway>;
let inventoryService: Mocked<InventoryService>;
beforeAll(async () => {
const { unit, unitRef } = await TestBed.solitary(OrderService).compile();
orderService = unit;
paymentGateway = unitRef.get(PaymentGateway);
inventoryService = unitRef.get(InventoryService);
});
it('should process order with payment', async () => {
inventoryService.checkStock.mockResolvedValue({ total: 100 });
paymentGateway.charge.mockResolvedValue({ success: true });
const result = await orderService.processOrder('123');
expect(result.success).toBe(true);
expect(paymentGateway.charge).toHaveBeenCalledWith(100);
});
});
Sociable Testing with Collaboration
Test classes with their real collaborators while mocking external boundaries:
@injectable()
class PriceCalculator {
calculateTotal(items: Item[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
}
@injectable()
class OrderService {
constructor(
private calculator: PriceCalculator,
private paymentGateway: PaymentGateway
) {}
async processOrder(items: Item[]) {
const total = this.calculator.calculateTotal(items);
return this.paymentGateway.charge(total);
}
}
import { TestBed, type Mocked } from '@suites/unit';
describe('Order Service Sociable Unit Spec', () => {
let orderService: OrderService;
let paymentGateway: Mocked<PaymentGateway>;
beforeAll(async () => {
const { unit, unitRef } = await TestBed.sociable(OrderService)
.collaborate() // All business logic collaborates
.exclude([PaymentGateway]) // Exclude external services
.compile();
orderService = unit;
paymentGateway = unitRef.get(PaymentGateway);
});
it('should calculate and charge correct total', async () => {
paymentGateway.charge.mockResolvedValue({ success: true });
await orderService.processOrder([
{ price: 10 },
{ price: 20 }
]);
// Real calculator logic executed, only gateway is mocked
expect(paymentGateway.charge).toHaveBeenCalledWith(30);
});
});
Token Dependencies
Token-injected dependencies are always auto-mocked (read more about Dependency Inversion):
@injectable()
class PaymentService {
constructor(
private validator: PaymentValidator, // Class dependency
@inject('DATABASE') private database: Database, // Token dependency
) {}
}
describe('PaymentService', () => {
let service: PaymentService;
let validator: Mocked<PaymentValidator>;
let database: Mocked<Database>;
beforeAll(async () => {
const { unit, unitRef } = await TestBed.solitary(PaymentService).compile();
service = unit;
validator = unitRef.get(PaymentValidator); // By class
database = unitRef.get<Database>('DATABASE'); // By token
});
});
InversifyJS Metadata Support
Suites fully supports InversifyJS metadata patterns for tagged and named bindings:
@injectable()
class Ninja {
constructor(
@inject('Weapon') @tagged('canThrow', false) private katana: Weapon,
@inject('Weapon') @tagged('canThrow', true) private shuriken: Weapon
) {}
}
describe('Ninja', () => {
let ninja: Ninja;
let katana: Mocked<Weapon>;
let shuriken: Mocked<Weapon>;
beforeAll(async () => {
const { unit, unitRef } = await TestBed.solitary(Ninja).compile();
ninja = unit;
// Access mocks by tag metadata
katana = unitRef.get<Weapon>('Weapon', { canThrow: false });
shuriken = unitRef.get<Weapon>('Weapon', { canThrow: true });
});
it('should use correct weapons', () => {
// Both mocks are fully typed and independent
expect(katana).toBeDefined();
expect(shuriken).toBeDefined();
});
});
You can also configure mocks with metadata before compilation:
await TestBed.solitary(Ninja)
.mock<Weapon>('Weapon', { canThrow: true })
.impl(stub => ({
attack: stub.mockReturnValue('shuriken attack!')
}))
.compile();
How it works
Suites uses a virtual DI container that analyzes Inversify's classes without initializing an actual container.
It:
- Detects
@injectableand@injectdecorators automatically - Creates type-safe mocks for all dependencies
- Wires them together without framework overhead
- Ensures token dependencies are always mocked (keeps tests as unit tests, not integration tests)
This means you get the testing ergonomics of a DI framework without the initialization cost or complexity.
Documentation
Full documentation with guides, API references, and examples: