MockComponent API ​
Asena's built-in testing utilities provide automated dependency mocking for components using dependency injection. The mockComponent and mockComponentAsync functions automatically discover and mock all injected dependencies.
Quick Start ​
import { mockComponent } from '@asenajs/asena/test';
import { Service } from '@asenajs/asena/decorators';
import { Inject } from '@asenajs/asena/decorators/ioc';
import { mock } from 'bun:test';
@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);
// Configure mock behavior
mocks.userService.createUser.mockResolvedValue({
id: 'user-123',
name: 'John Doe',
email: 'john@example.com'
});
// Test
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');
});
});API Reference ​
mockComponent ​
Creates a component instance with all dependencies automatically mocked.
function mockComponent<T extends object>(
ComponentClass: new (...args: any[]) => T,
options?: MockComponentOptions
): MockedComponent<T>Parameters:
ComponentClass- The component class to instantiateoptions- Optional configuration (see MockComponentOptions)
Returns: MockedComponent<T>
Example:
const { instance, mocks } = mockComponent(PaymentService);mockComponentAsync ​
Asynchronous version of mockComponent for components with async postConstruct hooks.
async function mockComponentAsync<T extends object>(
ComponentClass: new (...args: any[]) => T,
options?: MockComponentOptions
): Promise<MockedComponent<T>>Parameters: Same as mockComponent
Returns: Promise<MockedComponent<T>>
Example:
import { Service } from '@asenajs/asena/decorators';
import { Inject } from '@asenajs/asena/decorators/ioc';
@Service()
class ConnectionPool {
async connect() {
// implementation
}
}
@Service()
class DatabaseService {
@Inject(ConnectionPool)
private pool!: ConnectionPool;
async initialize() {
await this.pool.connect();
}
}
const { instance, mocks } = await mockComponentAsync(DatabaseService, {
postConstruct: async (inst) => {
await inst.initialize();
}
});MockComponentOptions ​
Configuration options for component mocking.
interface MockComponentOptions {
// Only mock specific fields (optional)
injections?: string[];
// Provide custom mocks instead of auto-generated ones (optional)
overrides?: Record<string, any>;
// Your own callback, run after injection (optional, can be async).
// NOT the component's @OnStart - see below
postConstruct?: (instance: any) => void | Promise<void>;
}Properties:
injections ​
Array of field names to mock. Other fields will not be mocked.
const { instance, mocks } = mockComponent(PaymentService, {
injections: ['stripe'] // Only mock stripe
});
expect(mocks.stripe).toBeDefined();
expect(mocks.logger).toBeUndefined();overrides ​
Custom mock objects to use instead of auto-generated mocks.
import { mock } from 'bun:test';
const customMock = {
createUser: mock(async () => ({ id: 'custom-id' }))
};
const { instance, mocks } = mockComponent(AuthService, {
overrides: { userService: customMock }
});
// mocks.userService is now your custom mock
expect(mocks.userService).toBe(customMock);An override is the final value injected into the field:
- For expression-based injections (e.g.
@Inject(ulak('/chat'))or@Inject(UserService, (s) => s.createUser)), the expression is skipped entirely — your override is used as-is. - Presence is checked with
Object.hasOwn, so falsy values (0,'',null,undefined) are injected as-is rather than ignored.
postConstruct ​
A callback of yours, run after the dependencies are injected.
const { instance, mocks } = mockComponent(AuthService, {
postConstruct: (instance) => {
console.log('Component ready for testing');
}
});It is not the component's @OnStart
mockComponent builds the instance directly — it never goes through the container or the server, so the component's own @OnStart / @OnStop hooks are not invoked. This option is the hook you would otherwise write inline; if you want the real start hook, call it yourself:
const { instance } = await mockComponentAsync(DatabaseService, {
postConstruct: async (inst) => inst.onStart(), // the @OnStart method, called explicitly
});For hooks running in their real order against a real container, use createTestApp.
Async Support
The postConstruct hook can be async. Use mockComponentAsync when you need to await the hook.
MockedComponent<T> ​
Return type of mockComponent and mockComponentAsync.
interface MockedComponent<T> {
instance: T; // Component instance with injected mocks
mocks: Record<string, any>; // Object containing all mock dependencies
}Properties:
instance- The component instance with all dependencies injectedmocks- Object where keys are field names and values are mock objects
Advanced Usage Patterns ​
Selective Mocking ​
Mock only specific dependencies while leaving others undefined.
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;
}
const { instance, mocks } = mockComponent(PaymentService, {
injections: ['stripe'] // Only mock stripe, logger remains undefined
});
expect(mocks.stripe).toBeDefined();
expect(mocks.logger).toBeUndefined();Custom Overrides ​
Provide your own mock implementations for specific dependencies.
import { mock } from 'bun:test';
const customUserService = {
createUser: mock(async (name: string, email: string) => ({
id: 'custom-id',
name,
email
})),
deleteUser: mock(async (id: string) => true)
};
const { instance, mocks } = mockComponent(AuthService, {
overrides: {
userService: customUserService
}
});
// mocks.userService is now your custom mock
expect(mocks.userService).toBe(customUserService);Combining Options ​
You can combine injections, overrides, and postConstruct together.
const { instance, mocks } = mockComponent(AuthService, {
injections: ['userService', 'emailService'],
overrides: {
userService: customUserService
},
postConstruct: (inst) => {
inst.setTestMode(true);
}
});Expression Transformations ​
mockComponent supports @Inject expression transformations automatically. When an expression field is not overridden, the expression is evaluated against a deep mock: every property access yields a Bun mock function and every call returns another chainable deep mock, so any expression works without a running application.
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);
// The expression is applied automatically - createUserFn is a real Bun mock
mocks.createUserFn.mockResolvedValue({ id: 'user-123' });
expect(mocks.createUserFn).toHaveBeenCalledWith('John', 'john@example.com');If the field is provided via overrides, the expression is skipped and your override is injected as-is.
Testing Services with Ulak Injections ​
Services that inject scoped namespaces with the ulak() helper are testable without a running WebSocket broker.
import { Service } from '@asenajs/asena/decorators';
import { Inject } from '@asenajs/asena/decorators/ioc';
import { ulak, type Ulak } from '@asenajs/asena/messaging';
@Service('UserService')
export class UserService {
@Inject(ulak('/ws/public/stats'))
private statsChannel: Ulak.NameSpace<'/ws/public/stats'>;
async createAnonUser(name: string) {
// ...create the user...
await this.statsChannel.broadcast({ action: 'update', data: { newUser: 1 } });
}
}Variant A — automatic deep mock. No configuration needed; the namespace methods are assertable Bun mocks:
import { describe, expect, test } from 'bun:test';
import { mockComponent } from '@asenajs/asena/test';
test('broadcasts stats on user creation', async () => {
const { instance, mocks } = mockComponent(UserService);
await instance.createAnonUser('John');
expect(mocks.statsChannel.broadcast).toHaveBeenCalledWith({
action: 'update',
data: { newUser: 1 }
});
});Variant B — typed stub via createTestUlakStub. Use an explicit override when you want a fully typed Ulak.NameSpace mock:
import { createTestUlakStub, mockComponent } from '@asenajs/asena/test';
test('broadcasts stats on user creation', async () => {
const statsChannel = createTestUlakStub('/ws/public/stats');
const { instance } = mockComponent(UserService, {
overrides: { statsChannel }
});
await instance.createAnonUser('John');
expect(statsChannel.broadcast).toHaveBeenCalledWith({
action: 'update',
data: { newUser: 1 }
});
});TIP
createTestUlakStub implements the full Ulak.NameSpace interface (broadcast, to, toSocket, toMany, getSocketCount) with Bun mocks, so it stays in sync with the framework at compile time.
Testing Components with Inheritance ​
mockComponent properly handles prototype chains.
import { Service } from '@asenajs/asena/decorators';
import { Inject } from '@asenajs/asena/decorators/ioc';
@Service()
class LoggerService {
log(message: string) {
// implementation
}
}
@Service()
class DatabaseService {
query(sql: string) {
// implementation
}
}
@Service()
class BaseService {
@Inject(LoggerService)
protected logger!: LoggerService;
}
@Service()
class UserService extends BaseService {
@Inject(DatabaseService)
private database!: DatabaseService;
}
const { instance, mocks } = mockComponent(UserService);
// Both inherited and own dependencies are mocked
expect(mocks.logger).toBeDefined();
expect(mocks.database).toBeDefined();Technical Details ​
How It Works ​
- Metadata Discovery - Reads the same metadata that Asena's IoC Container uses (
ComponentConstants.DependencyKey,ComponentConstants.DependencyClassKeyandComponentConstants.ExpressionKey) - Mock Generation - Uses Bun's native
mock()function to create mocks- Automatically detects async methods and creates
mock(async () => null) - Sync methods get
mock(() => undefined)
- Automatically detects async methods and creates
- Injection - Injects mocks into the component instance
- Expression Support - Evaluates expression transformations against a deep mock (unless the field is overridden, in which case the override is injected as-is)
What each field receives ​
The mock a field gets depends on how it was injected:
| Injection | Mock |
|---|---|
@Inject(UserService) | An object shaped like the class — every method is a bun:test mock |
@Inject(ulak('/chat')) and other expression injections | The expression evaluated against a deep mock, so any call chain works and stays assertable |
@Inject('UserService') | A plain {} — a string carries no class reference, so no method shape can be derived |
String injections need overrides
Only class-based injections can be auto-shaped. For @Inject('UserService') pass the double yourself:
mockComponent(LegacyService, {
overrides: { userService: { findById: mock(async () => ({ id: '1' })) } },
});Zero Dependencies ​
This feature follows Asena's zero-dependency philosophy:
- Uses only Bun's native
mock()function frombun:test - No external testing libraries required
- Fully compatible with Bun's test runner
Import Path ​
import {
mockComponent,
mockComponentAsync,
createMockFromClass,
createDeepMock,
createTestUlakStub
} from '@asenajs/asena/test';Package export configuration:
{
"exports": {
"./test": {
"import": "./dist/lib/test/index.js",
"types": "./dist/lib/test/index.d.ts"
}
}
}Related ​
- Testing Overview - Introduction to testing in Asena
- createTestApp - Full-application testing with real HTTP
- createWebTest - Controller-slice testing (note:
mocksthere is keyed by service name, not field name) - Examples - Real-world testing patterns
- Dependency Injection - Understanding DI in Asena
- Bun Test Documentation - Learn more about Bun's test runner