Swap and mock functionality for replacing dependencies during testing, with easy restoration capabilities. The testing support allows you to replace real implementations with test doubles for isolated unit testing.
Define fake implementations for class constructors during testing.
/**
* Defines a fake implementation for a class constructor
* @param binding - The class constructor to swap
* @param resolver - Factory function for the fake implementation
*/
swap<Binding extends AbstractConstructor<any>>(
binding: Binding,
resolver: BindingResolver<KnownBindings, InstanceType<Binding>>
): void;Usage Examples:
import { Container, inject } from "@adonisjs/fold";
// Production classes
class EmailService {
async send(to: string, message: string) {
// Real email sending logic
console.log(`Sending email to ${to}: ${message}`);
}
}
class Logger {
info(message: string) {
console.log(`[INFO] ${message}`);
}
}
@inject()
class UserService {
constructor(
private emailService: EmailService,
private logger: Logger
) {}
async createUser(userData: any) {
this.logger.info("Creating user");
const user = { id: 1, ...userData };
await this.emailService.send(user.email, "Welcome!");
return user;
}
}
const container = new Container();
// Test setup with swaps
describe("UserService", () => {
beforeEach(() => {
// Swap EmailService with a fake
container.swap(EmailService, () => ({
send: jest.fn().mockResolvedValue(undefined)
}));
// Swap Logger with a test double
container.swap(Logger, () => ({
info: jest.fn()
}));
});
afterEach(() => {
// Restore original implementations
container.restoreAll();
});
test("creates user with mocked dependencies", async () => {
const userService = await container.make(UserService);
const result = await userService.createUser({ email: "test@example.com" });
expect(result).toEqual({ id: 1, email: "test@example.com" });
});
});Restore specific bindings by removing their swaps.
/**
* Restores a binding by removing its swap
* @param binding - The class constructor to restore
*/
restore(binding: AbstractConstructor<any>): void;Usage Examples:
import { Container } from "@adonisjs/fold";
const container = new Container();
// Mock implementations
class MockEmailService {
async send() { return "mocked"; }
}
class MockLogger {
info() { console.log("mocked log"); }
}
// Set up swaps
container.swap(EmailService, () => new MockEmailService());
container.swap(Logger, () => new MockLogger());
// Test with both mocks
let userService = await container.make(UserService);
// UserService uses both MockEmailService and MockLogger
// Restore only EmailService
container.restore(EmailService);
// Test with partial restoration
userService = await container.make(UserService);
// UserService uses real EmailService but MockLogger
// Restore Logger as well
container.restore(Logger);
userService = await container.make(UserService);
// UserService uses both real implementationsRestore multiple or all bindings by removing their swaps.
/**
* Restores multiple or all bindings by removing their swaps
* @param bindings - Array of class constructors to restore, or undefined to restore all
*/
restoreAll(bindings?: AbstractConstructor<any>[]): void;Usage Examples:
import { Container } from "@adonisjs/fold";
const container = new Container();
// Set up multiple swaps
container.swap(EmailService, () => new MockEmailService());
container.swap(Logger, () => new MockLogger());
container.swap(Database, () => new MockDatabase());
container.swap(Cache, () => new MockCache());
// Restore specific bindings
container.restoreAll([EmailService, Logger]);
// Database and Cache swaps remain
// Restore all remaining swaps
container.restoreAll();
// All original implementations restored
// Common testing pattern
describe("Integration Tests", () => {
beforeEach(() => {
// Set up all mocks
container.swap(EmailService, () => new MockEmailService());
container.swap(Logger, () => new MockLogger());
container.swap(Database, () => new InMemoryDatabase());
});
afterEach(() => {
// Clean up all mocks
container.restoreAll();
});
test("test case", async () => {
// Test logic with mocked dependencies
});
});Understanding how swaps interact with the binding resolution system.
Resolution Priority:
Usage Examples:
import { Container } from "@adonisjs/fold";
const container = new Container();
// Various binding types for the same class
container.bindValue(Logger, new Logger("value_logger"));
container.bind(Logger, () => new Logger("bind_logger"));
container.singleton(Logger, () => new Logger("singleton_logger"));
// Without swap
let logger = await container.make(Logger);
console.log(logger); // Logger("value_logger") - bindValue wins
// With swap - overrides everything
container.swap(Logger, () => new Logger("swap_logger"));
logger = await container.make(Logger);
console.log(logger); // Logger("swap_logger") - swap wins
// Restore swap
container.restore(Logger);
logger = await container.make(Logger);
console.log(logger); // Logger("value_logger") - back to bindValueCommon patterns for testing with dependency injection.
Usage Examples:
import { Container, inject } from "@adonisjs/fold";
// Test helper class
class TestContainer {
private container: Container;
private swaps: AbstractConstructor<any>[] = [];
constructor() {
this.container = new Container();
}
mock<T>(binding: AbstractConstructor<T>, implementation: Partial<T>): void {
this.container.swap(binding, () => implementation as T);
this.swaps.push(binding);
}
spy<T>(binding: AbstractConstructor<T>): T {
const spy = jest.createMockFromModule(binding.name) as T;
this.mock(binding, spy);
return spy;
}
make<T>(binding: any): Promise<T> {
return this.container.make(binding);
}
cleanup(): void {
this.container.restoreAll(this.swaps);
this.swaps = [];
}
}
// Usage in tests
describe("UserService", () => {
let testContainer: TestContainer;
let emailSpy: EmailService;
let loggerSpy: Logger;
beforeEach(() => {
testContainer = new TestContainer();
// Create spies
emailSpy = testContainer.spy(EmailService);
loggerSpy = testContainer.spy(Logger);
// Mock specific methods
testContainer.mock(Database, {
query: jest.fn().mockResolvedValue([]),
insert: jest.fn().mockResolvedValue({ id: 1 })
});
});
afterEach(() => {
testContainer.cleanup();
});
test("user creation calls email service", async () => {
const userService = await testContainer.make(UserService);
await userService.createUser({ email: "test@example.com" });
expect(emailSpy.send).toHaveBeenCalledWith(
"test@example.com",
"Welcome!"
);
expect(loggerSpy.info).toHaveBeenCalledWith("Creating user");
});
});Complex testing scenarios with conditional mocks and partial implementations.
Usage Examples:
import { Container, inject } from "@adonisjs/fold";
// Conditional mocking based on test environment
class TestHelper {
static setupMocks(container: Container, options: {
mockEmail?: boolean;
mockDatabase?: boolean;
mockCache?: boolean;
}) {
if (options.mockEmail) {
container.swap(EmailService, () => ({
send: jest.fn().mockResolvedValue(undefined)
}));
}
if (options.mockDatabase) {
container.swap(Database, () => new InMemoryDatabase());
}
if (options.mockCache) {
container.swap(Cache, () => new Map());
}
}
}
describe("User Registration Flow", () => {
let container: Container;
beforeEach(() => {
container = new Container();
});
afterEach(() => {
container.restoreAll();
});
describe("Unit Tests", () => {
beforeEach(() => {
// Mock all external dependencies
TestHelper.setupMocks(container, {
mockEmail: true,
mockDatabase: true,
mockCache: true
});
});
test("isolated user creation logic", async () => {
// Test with all dependencies mocked
});
});
describe("Integration Tests", () => {
beforeEach(() => {
// Mock only external services, use real database
TestHelper.setupMocks(container, {
mockEmail: true,
mockCache: false
});
});
test("user creation with real database", async () => {
// Test with partial mocking
});
});
});