Testing ​
Asena provides built-in testing utilities that automatically mock dependencies for components using dependency injection. These utilities are designed exclusively for Bun's test runner.
Built for Bun
Asena's testing utilities work exclusively with Bun's native test runner. They use Bun's mock() function from bun:test and are not compatible with other testing frameworks.
Bun Test Runner ​
Asena's testing utilities integrate with Bun's built-in test runner, which provides:
- Native Performance - No transpilation, runs TypeScript directly
- Jest-Compatible API - Familiar
describe,test,expectsyntax - Built-in Mocking - Native
mock()function - Watch Mode - Automatic re-running on file changes
- Zero Configuration - Works out of the box with TypeScript
# Run all tests
bun test
# Watch mode
bun test --watch
# Specific test file
bun test ./tests/auth.test.ts
# With coverage
bun test --coverageAutomatic Dependency Mocking ​
The mockComponent function automatically discovers and mocks all injected dependencies:
import { describe, test, expect } from 'bun:test';
import { mockComponent } from '@asenajs/asena/test';
import { Service } from '@asenajs/asena/decorators';
import { Inject } from '@asenajs/asena/decorators/ioc';
@Service()
class UserService {
async createUser(name: string, email: string): Promise<{ id: string; name: string; email: string }> {
// implementation
}
}
@Service()
class AuthService {
@Inject(UserService)
private userService!: UserService;
async register(name: string, email: string, password: string) {
const user = await this.userService.createUser(name, email);
return { user, token: 'jwt-token' };
}
}
// Test
describe('AuthService', () => {
test('should register user', async () => {
const { instance, mocks } = mockComponent(AuthService);
mocks.userService.createUser.mockResolvedValue({
id: 'user-123',
name: 'John Doe',
email: 'john@example.com'
});
const result = await instance.register('John', 'john@example.com', 'pass');
expect(result.user.id).toBe('user-123');
expect(mocks.userService.createUser).toHaveBeenCalledWith('John', 'john@example.com');
});
});Two Levels of Testing ​
Asena ships utilities for both ends of the spectrum, all from @asenajs/asena/test:
| Use it when | Container | Adapter | |
|---|---|---|---|
mockComponent | Testing one class's logic in isolation | Bypassed | None |
createWebTest | Testing a controller's routing, middlewares and validation | Real, non-web deps auto-mocked | Real |
createTestApp | Testing the whole application end to end | Real | Real |
mockComponent is the fastest and covers most service-level tests. Reach for the harness when the thing you want to assert on is the framework's behaviour — a route matching, a middleware short-circuiting, a validator rejecting a bad payload.
// Unit: no framework involved
const { instance, mocks } = mockComponent(AuthService);
// Slice: real routing and validation, mocked services
const { app, mocks } = await createWebTest({ adapter, controllers: [UserController] });
await app.get('/api/users/1').expectStatus(200);
// Full: everything real, swap what you need
await using app = await createTestApp({
adapter,
components: [UserController, UserService],
overrides: { UserService: myDouble },
});Import Path ​
// Unit-level mocking
import { mockComponent, mockComponentAsync, createDeepMock, createTestUlakStub } from '@asenajs/asena/test';
// Integration harness
import { createTestApp, createWebTest, silentLogger } from '@asenajs/asena/test';What You Can Test ​
Services with Dependency Injection ​
import { Service } from '@asenajs/asena/decorators';
import { Inject } from '@asenajs/asena/decorators/ioc';
@Service()
class StripeClient {
async charge(amount: number) {
// implementation
}
}
@Service()
class LoggerService {
log(message: string) {
// implementation
}
}
@Service()
class PaymentService {
@Inject(StripeClient)
private stripe!: StripeClient;
@Inject(LoggerService)
private logger!: LoggerService;
async processPayment(amount: number) {
this.logger.log(`Processing payment: ${amount}`);
return await this.stripe.charge(amount);
}
}
const { instance, mocks } = mockComponent(PaymentService);
// Both stripe and logger are automatically mockedControllers ​
import { Controller } from '@asenajs/asena/decorators';
import { Inject } from '@asenajs/asena/decorators/ioc';
import { Get } from '@asenajs/asena/decorators/http';
import type { Context } from '@asenajs/ergenecore';
@Service()
class UserService {
async getUser(id: string) {
// implementation
}
}
@Controller('/users')
class UserController {
@Inject(UserService)
private userService!: UserService;
@Get('/:id')
async getUser(context: Context) {
const id = context.getParam('id');
return this.userService.getUser(id);
}
}
const { instance, mocks } = mockComponent(UserController);WebSocket Services ​
import { WebSocket } from '@asenajs/asena/decorators';
import { Inject } from '@asenajs/asena/decorators/ioc';
import { AsenaWebSocketService, type Socket } from '@asenajs/asena/web-socket';
@Service()
class MessageService {
async saveMessage(message: string) {
// implementation
}
}
@WebSocket('/chat')
class ChatSocket extends AsenaWebSocketService {
@Inject(MessageService)
private messageService!: MessageService;
async onMessage(ws: Socket, message: string) {
await this.messageService.saveMessage(message);
}
}
const { instance, mocks } = mockComponent(ChatSocket);Main Testing Utilities ​
mockComponent ​
Synchronous component instantiation with automatic dependency mocking.
const { instance, mocks } = mockComponent(AuthService);Returns an object with:
instance- The component instance with all dependencies mockedmocks- Object containing all mock dependencies
See the MockComponent API documentation for detailed usage.
mockComponentAsync ​
Asynchronous version, for when the postConstruct callback you pass is async. It is your callback, not the component's @OnStart — mockComponent never runs the component's own lifecycle hooks.
@Service()
class DatabaseService {
@Inject(ConnectionPool)
private pool!: ConnectionPool;
async initialize() {
// async initialization
}
}
const { instance, mocks } = await mockComponentAsync(DatabaseService, {
postConstruct: async (inst) => {
await inst.initialize();
}
});Advanced Features ​
Selective Mocking ​
Mock only specific dependencies:
const { instance, mocks } = mockComponent(PaymentService, {
injections: ['stripe'] // Only mock stripe, logger remains undefined
});
expect(mocks.stripe).toBeDefined();
expect(mocks.logger).toBeUndefined();Custom Overrides ​
Provide custom mock implementations:
import { mock } from 'bun:test';
const customUserService = {
createUser: mock(async (name: string, email: string) => ({
id: 'custom-id',
name,
email
}))
};
const { instance, mocks } = mockComponent(AuthService, {
overrides: { userService: customUserService }
});Expression Transformations ​
Supports @Inject expression transformations:
import { Service } from '@asenajs/asena/decorators';
import { Inject } from '@asenajs/asena/decorators/ioc';
@Service()
class UserService {
async createUser(name: string, email: string): Promise<{ id: string; name: string; email: string }> {
// implementation
}
}
@Service()
class AuthService {
@Inject(UserService, (service) => service.createUser)
private createUserFn!: (name: string, email: string) => Promise<User>;
}
const { instance, mocks } = mockComponent(AuthService);
mocks.createUserFn.mockResolvedValue({ id: 'user-123' });How It Works ​
- Metadata Discovery - Reads the same metadata that Asena's IoC Container uses
- Mock Generation - Uses Bun's native
mock()function to create mocks - Injection - Injects mocks into the component instance
- Expression Support - Applies expression transformations if defined
Next Steps ​
- MockComponent API - Complete API reference and advanced usage
- Examples - Real-world testing patterns for controllers, services, and WebSockets
- Bun Test Documentation - Learn more about Bun's test runner