Testing
Test Through the Same Boundary Production Uses
IoC is useful because construction is a seam. Tests should replace collaborators through container registration, then resolve and exercise the real class under test — no mocking framework needed.
Co-Located Mock
Keep reusable fakes next to the real boundary. Export a registration function that receives the test container.
import type { IContainer } from "@noego/ioc";
import { LoadAs } from "@noego/ioc";
import { PaymentProcessor } from "./payment_processor";
class MockPaymentProcessor extends PaymentProcessor {
async charge(amount: number): Promise<string> {
if (amount <= 0) {
throw new Error("amount must be positive");
}
return "mock-payment-id";
}
}
export function mockPaymentProcessor(container: IContainer): void {
container.registerFunction(
PaymentProcessor,
() => new MockPaymentProcessor(),
{ loadAs: LoadAs.Singleton },
);
}Test Shape
Create a fresh container, register the fake, resolve the class under test, and exercise it. No mocks, no module-level vi.mock() — just container registration.
import createContainer from "@noego/ioc";
import { OrderService } from "./order_service";
import { mockPaymentProcessor } from "./payment_processor.mock";
it("charges through the configured processor", async () => {
const container = createContainer();
mockPaymentProcessor(container);
const service = await container.instance(OrderService);
await expect(service.placeOrder({ amount: 25 })).resolves.toEqual({
status: "paid",
paymentId: "mock-payment-id",
});
});Testing Scoped Services
Services that consume runtime parameters need a child scope. Use container.extend() to create one, pass parameter values, and verify isolation between scopes.
import createContainer from "@noego/ioc";
import { CurrentUser } from "./current_user";
import { USER_ID, USER_ROLE } from "./tokens";
it("reads permissions based on role", async () => {
const root = createContainer();
const scope = root.extend();
const user = await scope.instance(CurrentUser, [
USER_ID.value("user-123"),
USER_ROLE.value("admin"),
]);
expect(user.role).toBe("admin");
});
it("isolates state between scopes", async () => {
const root = createContainer();
const scope1 = root.extend();
const scope2 = root.extend();
const user1 = await scope1.instance(CurrentUser, [
USER_ID.value("user-1"),
USER_ROLE.value("member"),
]);
const user2 = await scope2.instance(CurrentUser, [
USER_ID.value("user-2"),
USER_ROLE.value("admin"),
]);
expect(user1.role).toBe("member");
expect(user2.role).toBe("admin");
});Rules
- Create a fresh container per test.
- Register fakes before resolving the class under test.
- Resolve the real class through the container.
- Put reusable test fakes in co-located .mock.ts files.
- Make fakes enforce the same boundary rules as real implementations.
- Avoid vi.mock() for internal IoC-managed services.
- Use container.extend() to test scoped services with runtime parameters.