Testing
Learn how to test services effectively with the DI framework.
Basic Testing Setup
Create isolated test containers to avoid polluting the global container:
import { Container as DIContainer } from 'di-framework/container';
import { describe, it, expect, beforeEach } from 'bun:test';
describe('UserService', () => {
let testContainer: DIContainer;
beforeEach(() => {
// Create a fresh container for each test
testContainer = new DIContainer();
});
it('should create user', () => {
// Register services
testContainer.register(DatabaseService);
testContainer.register(UserService);
// Resolve and test
const userService = testContainer.resolve(UserService);
expect(userService).toBeDefined();
});
});
Mocking Dependencies
Replace real services with mock implementations:
// Mock implementation
class MockDatabaseService {
query(sql: string) {
return { rows: [{ id: '1', name: 'Test User' }] };
}
}
describe('UserService', () => {
let testContainer: DIContainer;
beforeEach(() => {
testContainer = new DIContainer();
// Register mock instead of real service
testContainer.register(MockDatabaseService);
testContainer.register(UserService);
});
it('should return user from database', () => {
const userService = testContainer.resolve(UserService);
const user = userService.getUser('1');
expect(user.rows[0].name).toBe('Test User');
});
});
Testing with Factory Services
Mock configuration and factory-registered services:
describe('UserService with config', () => {
let testContainer: DIContainer;
beforeEach(() => {
testContainer = new DIContainer();
// Register test configuration
testContainer.registerFactory('config', () => ({
apiUrl: 'http://test.example.com',
timeout: 1000
}), { singleton: true });
testContainer.register(UserService);
});
it('should use test configuration', () => {
const userService = testContainer.resolve(UserService);
const config = testContainer.resolve('config');
expect(config.apiUrl).toBe('http://test.example.com');
});
});
Testing Dependency Injection
Verify that dependencies are correctly injected:
describe('OrderService dependencies', () => {
it('should inject all required dependencies', () => {
const testContainer = new DIContainer();
testContainer.register(DatabaseService);
testContainer.register(PaymentService);
testContainer.register(EmailService);
testContainer.register(OrderService);
const orderService = testContainer.resolve(OrderService);
// Verify service is properly initialized
expect(orderService).toBeDefined();
expect(() => orderService.createOrder({})).not.toThrow();
});
});
Testing Singleton vs Transient
Verify singleton and transient behavior:
describe('Service lifecycle', () => {
it('should return same instance for singletons', () => {
const testContainer = new DIContainer();
testContainer.register(DatabaseService, { singleton: true });
const instance1 = testContainer.resolve(DatabaseService);
const instance2 = testContainer.resolve(DatabaseService);
expect(instance1).toBe(instance2);
});
it('should return different instances for transient services', () => {
const testContainer = new DIContainer();
testContainer.register(RequestContext, { singleton: false });
const instance1 = testContainer.resolve(RequestContext);
const instance2 = testContainer.resolve(RequestContext);
expect(instance1).not.toBe(instance2);
});
});
Reusing Setup with fork()
Share a seeded container across tests while keeping isolation:
import { Container as DIContainer } from 'di-framework/container';
const base = new DIContainer();
base.register(DatabaseService);
base.register(LoggerService);
beforeEach(() => {
testContainer = base.fork({ carrySingletons: true }); // reuse expensive singletons
});
Why: Avoid re-registering common services while ensuring tests cannot mutate each other's registrations.
Construct Instances with Overrides
Build ad-hoc instances for tests without registering them:
class Greeter {
constructor(@Component(LoggerService) private logger: LoggerService, private greeting: string) {}
}
it('should allow override of primitive args', () => {
const c = new DIContainer();
c.register(LoggerService);
const greeter = c.construct(Greeter, { 1: 'hello test' });
expect(greeter).toBeInstanceOf(Greeter);
});
Why: Handy for targeted unit tests where you need DI-managed dependencies plus specific literal parameters.
Testing Error Scenarios
Test error handling and validation:
describe('Error handling', () => {
it('should throw when service not registered', () => {
const testContainer = new DIContainer();
expect(() => {
testContainer.resolve(UnregisteredService);
}).toThrow('Service \'UnregisteredService\' is not registered');
});
it('should detect circular dependencies', () => {
const testContainer = new DIContainer();
testContainer.register(ServiceA);
testContainer.register(ServiceB);
expect(() => {
testContainer.resolve(ServiceA);
}).toThrow('Circular dependency detected');
});
});
Spy and Stub Pattern
Create spies to verify method calls:
class SpyDatabaseService {
queries: string[] = [];
query(sql: string) {
this.queries.push(sql);
return { rows: [] };
}
}
describe('UserService with spy', () => {
it('should call database with correct query', () => {
const testContainer = new DIContainer();
testContainer.register(SpyDatabaseService);
testContainer.register(UserService);
const userService = testContainer.resolve(UserService);
const spy = testContainer.resolve(SpyDatabaseService);
userService.getUser('123');
expect(spy.queries).toContain("SELECT * FROM users WHERE id = '123'");
});
});
Testing Async Services
Test services with async operations:
class MockAsyncDatabaseService {
async connect() {
return Promise.resolve();
}
async query(sql: string) {
return Promise.resolve({ rows: [{ id: '1' }] });
}
}
describe('Async UserService', () => {
it('should handle async operations', async () => {
const testContainer = new DIContainer();
testContainer.register(MockAsyncDatabaseService);
testContainer.register(UserService);
const userService = testContainer.resolve(UserService);
const user = await userService.getUserAsync('1');
expect(user.rows).toHaveLength(1);
});
});
Integration Testing
Test multiple services working together:
describe('Order processing integration', () => {
it('should process complete order flow', async () => {
const testContainer = new DIContainer();
// Register all required services
testContainer.register(MockDatabaseService);
testContainer.register(MockPaymentService);
testContainer.register(MockEmailService);
testContainer.register(OrderService);
const orderService = testContainer.resolve(OrderService);
const order = await orderService.createOrder({
userId: '1',
items: [{ id: 'item1', quantity: 2 }],
total: 100
});
expect(order.id).toBeDefined();
expect(order.status).toBe('completed');
});
});
Testing Best Practices
1. Use Isolated Containers
// Good - Fresh container per test
beforeEach(() => {
testContainer = new DIContainer();
});
// Bad - Shared container
const testContainer = new DIContainer(); // Global
2. Mock External Dependencies
// Good - Mock external services
class MockEmailService {
async sendEmail(to: string, subject: string) {
return { success: true, messageId: 'test-123' };
}
}
// Bad - Using real email service in tests
testContainer.register(RealEmailService); // Will send real emails
3. Test One Thing at a Time
// Good - Focused test
it('should validate user email', () => {
const validator = testContainer.resolve(UserValidator);
expect(() => validator.validateEmail('invalid')).toThrow();
});
// Bad - Testing multiple things
it('should create user and send email and log activity', () => {
// Too much in one test
});
4. Provide Clear Test Data
// Good - Clear test data
const testUser = {
id: '1',
name: 'Test User',
email: 'test@example.com'
};
// Bad - Unclear test data
const testUser = { id: '1', n: 'TU', e: 't@e.c' };
Complete Test Example
Here's a complete testing example:
import { Container as DIContainer } from 'di-framework/container';
import { describe, it, expect, beforeEach } from 'bun:test';
// Mock services
class MockDatabaseService {
private users = new Map([
['1', { id: '1', name: 'John Doe', email: 'john@example.com' }]
]);
query(sql: string) {
const match = sql.match(/id = '(\d+)'/);
if (match) {
const user = this.users.get(match[1]);
return { rows: user ? [user] : [] };
}
return { rows: [] };
}
}
class MockLoggerService {
logs: string[] = [];
log(message: string) {
this.logs.push(message);
}
}
// Test suite
describe('UserService', () => {
let testContainer: DIContainer;
beforeEach(() => {
testContainer = new DIContainer();
testContainer.register(MockDatabaseService);
testContainer.register(MockLoggerService);
testContainer.register(UserService);
});
it('should get user by id', () => {
const userService = testContainer.resolve(UserService);
const user = userService.getUser('1');
expect(user.rows[0].name).toBe('John Doe');
});
it('should log user retrieval', () => {
const userService = testContainer.resolve(UserService);
const logger = testContainer.resolve(MockLoggerService);
userService.getUser('1');
expect(logger.logs).toContain('Getting user: 1');
});
it('should return empty for non-existent user', () => {
const userService = testContainer.resolve(UserService);
const user = userService.getUser('999');
expect(user.rows).toHaveLength(0);
});
});
Last modified: 23 November 2025