Instructions for writing tests for elizaOS plugins using Bun’s test runner
src/
__tests__/
test-utils.ts # Shared test utilities and mocks
index.test.ts # Main plugin tests
actions.test.ts # Action tests
providers.test.ts # Provider tests
evaluators.test.ts # Evaluator tests
services.test.ts # Service tests
actions/
providers/
evaluators/
services/
index.ts
{
"devDependencies": {
"@types/bun": "latest",
"bun-types": "latest"
}
}
import { describe, expect, it, mock, beforeEach, afterEach, spyOn } from 'bun:test';
import {
type IAgentRuntime,
type Memory,
type State,
type HandlerCallback,
type Action,
type Provider,
type Evaluator,
ModelType,
logger,
} from '@elizaos/core';
test-utils.ts
file with reusable mock objects and helper functions:
import { mock } from 'bun:test';
import {
type IAgentRuntime,
type Memory,
type State,
type Character,
type UUID,
type Content,
type Room,
type Entity,
ChannelType,
} from '@elizaos/core';
// Mock Runtime Type
export type MockRuntime = Partial<IAgentRuntime> & {
agentId: UUID;
character: Character;
getSetting: ReturnType<typeof mock>;
useModel: ReturnType<typeof mock>;
composeState: ReturnType<typeof mock>;
createMemory: ReturnType<typeof mock>;
getMemories: ReturnType<typeof mock>;
searchMemories: ReturnType<typeof mock>;
updateMemory: ReturnType<typeof mock>;
getRoom: ReturnType<typeof mock>;
getParticipantUserState: ReturnType<typeof mock>;
setParticipantUserState: ReturnType<typeof mock>;
emitEvent: ReturnType<typeof mock>;
getTasks: ReturnType<typeof mock>;
providers: any[];
actions: any[];
evaluators: any[];
services: any[];
};
// Create Mock Runtime
export function createMockRuntime(overrides: Partial<MockRuntime> = {}): MockRuntime {
return {
agentId: 'test-agent-id' as UUID,
character: {
name: 'Test Agent',
bio: 'A test agent for unit testing',
templates: {
messageHandlerTemplate: 'Test template {{recentMessages}}',
shouldRespondTemplate: 'Should respond {{recentMessages}}',
},
} as Character,
// Core methods with default implementations
useModel: mock().mockResolvedValue('Mock response'),
composeState: mock().mockResolvedValue({
values: {
agentName: 'Test Agent',
recentMessages: 'Test message',
},
data: {
room: {
id: 'test-room-id',
type: ChannelType.DIRECT,
},
},
}),
createMemory: mock().mockResolvedValue({ id: 'memory-id' }),
getMemories: mock().mockResolvedValue([]),
searchMemories: mock().mockResolvedValue([]),
updateMemory: mock().mockResolvedValue(undefined),
getSetting: mock().mockImplementation((key: string) => {
const settings: Record<string, string> = {
TEST_SETTING: 'test-value',
API_KEY: 'test-api-key',
// Add common settings your plugin might need
};
return settings[key];
}),
getRoom: mock().mockResolvedValue({
id: 'test-room-id',
type: ChannelType.DIRECT,
worldId: 'test-world-id',
serverId: 'test-server-id',
source: 'test',
}),
getParticipantUserState: mock().mockResolvedValue('ACTIVE'),
setParticipantUserState: mock().mockResolvedValue(undefined),
emitEvent: mock().mockResolvedValue(undefined),
getTasks: mock().mockResolvedValue([]),
// Provider/action/evaluator lists
providers: [],
actions: [],
evaluators: [],
services: [],
// Override with custom implementations
...overrides,
};
}
// Create Mock Memory
export function createMockMemory(overrides: Partial<Memory> = {}): Partial<Memory> {
return {
id: 'test-message-id' as UUID,
roomId: 'test-room-id' as UUID,
entityId: 'test-entity-id' as UUID,
agentId: 'test-agent-id' as UUID,
content: {
text: 'Test message',
channelType: ChannelType.DIRECT,
source: 'direct',
} as Content,
createdAt: Date.now(),
userId: 'test-user-id' as UUID,
...overrides,
};
}
// Create Mock State
export function createMockState(overrides: Partial<State> = {}): Partial<State> {
return {
values: {
agentName: 'Test Agent',
recentMessages: 'User: Test message',
...overrides.values,
},
data: {
room: {
id: 'test-room-id',
type: ChannelType.DIRECT,
},
...overrides.data,
},
...overrides,
};
}
// Setup Action Test Helper
export function setupActionTest(
options: {
runtimeOverrides?: Partial<MockRuntime>;
messageOverrides?: Partial<Memory>;
stateOverrides?: Partial<State>;
} = {}
) {
const mockRuntime = createMockRuntime(options.runtimeOverrides);
const mockMessage = createMockMemory(options.messageOverrides);
const mockState = createMockState(options.stateOverrides);
const callbackFn = mock().mockResolvedValue([]);
return {
mockRuntime,
mockMessage,
mockState,
callbackFn,
};
}
// Mock Logger Helper
export function mockLogger() {
spyOn(logger, 'error').mockImplementation(() => {});
spyOn(logger, 'warn').mockImplementation(() => {});
spyOn(logger, 'info').mockImplementation(() => {});
spyOn(logger, 'debug').mockImplementation(() => {});
}
// src/__tests__/actions.test.ts
import { describe, expect, it, mock, beforeEach, afterEach } from 'bun:test';
import { myAction } from '../actions/myAction';
import { setupActionTest, mockLogger } from './test-utils';
import type { MockRuntime } from './test-utils';
import {
type IAgentRuntime,
type Memory,
type State,
type HandlerCallback,
ModelType,
} from '@elizaos/core';
describe('My Action', () => {
let mockRuntime: MockRuntime;
let mockMessage: Partial<Memory>;
let mockState: Partial<State>;
let callbackFn: HandlerCallback;
beforeEach(() => {
mockLogger();
const setup = setupActionTest();
mockRuntime = setup.mockRuntime;
mockMessage = setup.mockMessage;
mockState = setup.mockState;
callbackFn = setup.callbackFn as HandlerCallback;
});
afterEach(() => {
mock.restore();
});
describe('validation', () => {
it('should validate when conditions are met', async () => {
// Setup message content that should validate
mockMessage.content = {
text: 'perform action',
channelType: 'direct',
};
const isValid = await myAction.validate(
mockRuntime as IAgentRuntime,
mockMessage as Memory,
mockState as State
);
expect(isValid).toBe(true);
});
it('should not validate when conditions are not met', async () => {
// Setup message content that should not validate
mockMessage.content = {
text: 'unrelated message',
channelType: 'direct',
};
const isValid = await myAction.validate(
mockRuntime as IAgentRuntime,
mockMessage as Memory,
mockState as State
);
expect(isValid).toBe(false);
});
});
describe('handler', () => {
it('should handle action successfully', async () => {
// Mock runtime methods specific to this action
mockRuntime.useModel = mock().mockResolvedValue({
action: 'PERFORM',
parameters: { value: 'test' },
});
const result = await myAction.handler(
mockRuntime as IAgentRuntime,
mockMessage as Memory,
mockState as State,
{},
callbackFn
);
expect(result).toBe(true);
expect(callbackFn).toHaveBeenCalledWith(
expect.objectContaining({
text: expect.any(String),
content: expect.any(Object),
})
);
});
it('should handle errors gracefully', async () => {
// Mock an error scenario
mockRuntime.useModel = mock().mockRejectedValue(new Error('Model error'));
await myAction.handler(
mockRuntime as IAgentRuntime,
mockMessage as Memory,
mockState as State,
{},
callbackFn
);
expect(callbackFn).toHaveBeenCalledWith(
expect.objectContaining({
text: expect.stringContaining('error'),
})
);
});
});
});
describe('Async Action', () => {
it('should handle async operations', async () => {
const setup = setupActionTest({
runtimeOverrides: {
useModel: mock().mockImplementation(async (modelType) => {
// Simulate async delay
await new Promise((resolve) => setTimeout(resolve, 100));
return { result: 'async result' };
}),
},
});
const result = await asyncAction.handler(
setup.mockRuntime as IAgentRuntime,
setup.mockMessage as Memory,
setup.mockState as State,
{},
setup.callbackFn as HandlerCallback
);
expect(result).toBe(true);
expect(setup.callbackFn).toHaveBeenCalled();
});
});
// src/__tests__/providers.test.ts
import { describe, expect, it, mock, beforeEach, afterEach } from 'bun:test';
import { myProvider } from '../providers/myProvider';
import { createMockRuntime, createMockMemory, createMockState } from './test-utils';
import { type IAgentRuntime, type Memory, type State } from '@elizaos/core';
describe('My Provider', () => {
let mockRuntime: any;
let mockMessage: Partial<Memory>;
let mockState: Partial<State>;
beforeEach(() => {
mockRuntime = createMockRuntime();
mockMessage = createMockMemory();
mockState = createMockState();
});
afterEach(() => {
mock.restore();
});
it('should have required properties', () => {
expect(myProvider.name).toBe('MY_PROVIDER');
expect(myProvider.get).toBeDefined();
expect(typeof myProvider.get).toBe('function');
});
it('should return data in correct format', async () => {
// Mock any runtime methods the provider uses
mockRuntime.getMemories = mock().mockResolvedValue([
{ content: { text: 'Memory 1' }, createdAt: Date.now() },
{ content: { text: 'Memory 2' }, createdAt: Date.now() - 1000 },
]);
const result = await myProvider.get(
mockRuntime as IAgentRuntime,
mockMessage as Memory,
mockState as State
);
expect(result).toMatchObject({
text: expect.any(String),
data: expect.any(Object),
});
});
it('should handle empty data gracefully', async () => {
mockRuntime.getMemories = mock().mockResolvedValue([]);
const result = await myProvider.get(
mockRuntime as IAgentRuntime,
mockMessage as Memory,
mockState as State
);
expect(result).toBeDefined();
expect(result.text).toContain('No data available');
});
it('should handle errors gracefully', async () => {
mockRuntime.getMemories = mock().mockRejectedValue(new Error('Database error'));
const result = await myProvider.get(
mockRuntime as IAgentRuntime,
mockMessage as Memory,
mockState as State
);
expect(result).toBeDefined();
expect(result.text).toContain('Error retrieving data');
});
});
// src/__tests__/evaluators.test.ts
import { describe, expect, it, mock, beforeEach, afterEach } from 'bun:test';
import { myEvaluator } from '../evaluators/myEvaluator';
import { createMockRuntime, createMockMemory, createMockState } from './test-utils';
import { type IAgentRuntime, type Memory, type State } from '@elizaos/core';
describe('My Evaluator', () => {
let mockRuntime: any;
let mockMessage: Partial<Memory>;
let mockState: Partial<State>;
beforeEach(() => {
mockRuntime = createMockRuntime();
mockMessage = createMockMemory();
mockState = createMockState();
});
afterEach(() => {
mock.restore();
});
it('should have required properties', () => {
expect(myEvaluator.name).toBe('MY_EVALUATOR');
expect(myEvaluator.evaluate).toBeDefined();
expect(myEvaluator.validate).toBeDefined();
});
it('should validate when conditions are met', async () => {
const isValid = await myEvaluator.validate(
mockRuntime as IAgentRuntime,
mockMessage as Memory,
mockState as State
);
expect(isValid).toBe(true);
});
it('should evaluate and create memory', async () => {
mockRuntime.createMemory = mock().mockResolvedValue({ id: 'new-memory-id' });
await myEvaluator.evaluate(
mockRuntime as IAgentRuntime,
mockMessage as Memory,
mockState as State,
{}
);
expect(mockRuntime.createMemory).toHaveBeenCalledWith(
expect.objectContaining({
content: expect.objectContaining({
text: expect.any(String),
}),
}),
expect.any(String) // tableName
);
});
it('should not create memory when evaluation fails', async () => {
// Mock a scenario where evaluation should fail
mockMessage.content = { text: 'invalid content' };
await myEvaluator.evaluate(
mockRuntime as IAgentRuntime,
mockMessage as Memory,
mockState as State,
{}
);
expect(mockRuntime.createMemory).not.toHaveBeenCalled();
});
});
// src/__tests__/services.test.ts
import { describe, expect, it, mock, beforeEach, afterEach } from 'bun:test';
import { myService } from '../services/myService';
import { createMockRuntime } from './test-utils';
import { type IAgentRuntime } from '@elizaos/core';
describe('My Service', () => {
let mockRuntime: any;
beforeEach(() => {
mockRuntime = createMockRuntime();
});
afterEach(() => {
mock.restore();
});
it('should initialize service', async () => {
const service = await myService.initialize(mockRuntime as IAgentRuntime);
expect(service).toBeDefined();
expect(service.start).toBeDefined();
expect(service.stop).toBeDefined();
});
it('should start service successfully', async () => {
const service = await myService.initialize(mockRuntime as IAgentRuntime);
const startSpy = mock(service.start);
await service.start();
expect(startSpy).toHaveBeenCalled();
});
it('should stop service successfully', async () => {
const service = await myService.initialize(mockRuntime as IAgentRuntime);
await service.start();
const stopSpy = mock(service.stop);
await service.stop();
expect(stopSpy).toHaveBeenCalled();
});
it('should handle service errors', async () => {
const service = await myService.initialize(mockRuntime as IAgentRuntime);
service.start = mock().mockRejectedValue(new Error('Service start failed'));
await expect(service.start()).rejects.toThrow('Service start failed');
});
});
// src/__tests__/events.test.ts
import { describe, expect, it, mock, beforeEach, afterEach } from 'bun:test';
import { myPlugin } from '../index';
import { setupActionTest } from './test-utils';
import {
type IAgentRuntime,
type Memory,
EventType,
type MessagePayload,
type EntityPayload,
} from '@elizaos/core';
describe('Event Handlers', () => {
let mockRuntime: any;
let mockMessage: Partial<Memory>;
let mockCallback: any;
beforeEach(() => {
const setup = setupActionTest();
mockRuntime = setup.mockRuntime;
mockMessage = setup.mockMessage;
mockCallback = setup.callbackFn;
});
afterEach(() => {
mock.restore();
});
it('should handle MESSAGE_RECEIVED event', async () => {
const messageHandler = myPlugin.events?.[EventType.MESSAGE_RECEIVED]?.[0];
expect(messageHandler).toBeDefined();
if (messageHandler) {
await messageHandler({
runtime: mockRuntime as IAgentRuntime,
message: mockMessage as Memory,
callback: mockCallback,
source: 'test',
} as MessagePayload);
expect(mockRuntime.createMemory).toHaveBeenCalledWith(mockMessage, 'messages');
}
});
it('should handle ENTITY_JOINED event', async () => {
const entityHandler = myPlugin.events?.[EventType.ENTITY_JOINED]?.[0];
expect(entityHandler).toBeDefined();
if (entityHandler) {
await entityHandler({
runtime: mockRuntime as IAgentRuntime,
entityId: 'test-entity-id',
worldId: 'test-world-id',
roomId: 'test-room-id',
metadata: {
type: 'user',
username: 'testuser',
},
source: 'test',
} as EntityPayload);
expect(mockRuntime.ensureConnection).toHaveBeenCalled();
}
});
});
describe('Complex State Action', () => {
it('should handle complex state transformations', async () => {
const setup = setupActionTest({
stateOverrides: {
values: {
taskList: ['task1', 'task2'],
currentStep: 2,
metadata: { key: 'value' },
},
data: {
customData: {
nested: {
value: 'deep',
},
},
},
},
});
const result = await complexAction.handler(
setup.mockRuntime as IAgentRuntime,
setup.mockMessage as Memory,
setup.mockState as State,
{},
setup.callbackFn as HandlerCallback
);
expect(result).toBe(true);
});
});
describe('Sequential Operations', () => {
it('should handle sequential API calls', async () => {
const setup = setupActionTest({
runtimeOverrides: {
useModel: mock()
.mockResolvedValueOnce({ step: 1, data: 'first' })
.mockResolvedValueOnce({ step: 2, data: 'second' })
.mockResolvedValueOnce({ step: 3, data: 'final' }),
},
});
await sequentialAction.handler(
setup.mockRuntime as IAgentRuntime,
setup.mockMessage as Memory,
setup.mockState as State,
{},
setup.callbackFn as HandlerCallback
);
expect(setup.mockRuntime.useModel).toHaveBeenCalledTimes(3);
expect(setup.callbackFn).toHaveBeenCalledWith(
expect.objectContaining({
text: expect.stringContaining('final'),
})
);
});
});
describe('Error Recovery', () => {
it('should retry on failure', async () => {
let attempts = 0;
const setup = setupActionTest({
runtimeOverrides: {
useModel: mock().mockImplementation(async () => {
attempts++;
if (attempts < 3) {
throw new Error('Temporary failure');
}
return { success: true };
}),
},
});
await retryAction.handler(
setup.mockRuntime as IAgentRuntime,
setup.mockMessage as Memory,
setup.mockState as State,
{},
setup.callbackFn as HandlerCallback
);
expect(attempts).toBe(3);
expect(setup.callbackFn).toHaveBeenCalledWith(
expect.objectContaining({
content: expect.objectContaining({ success: true }),
})
);
});
});
describe
blocks// Good: Specific mocks for each test
it('should handle specific scenario', async () => {
const setup = setupActionTest({
runtimeOverrides: {
useModel: mock().mockResolvedValue({ specific: 'response' }),
},
});
// ... test implementation
});
// Bad: Global mocks that affect all tests
beforeAll(() => {
globalMock = mock().mockResolvedValue('global response');
});
// Check callback was called with correct structure
expect(callbackFn).toHaveBeenCalledWith(
expect.objectContaining({
text: expect.stringContaining('expected text'),
content: expect.objectContaining({
success: true,
data: expect.arrayContaining(['item1', 'item2']),
}),
})
);
// Check multiple calls in sequence
const calls = (callbackFn as any).mock.calls;
expect(calls).toHaveLength(3);
expect(calls[0][0].text).toContain('step 1');
expect(calls[1][0].text).toContain('step 2');
expect(calls[2][0].text).toContain('completed');
describe('Edge Cases', () => {
it('should handle empty input', async () => {
mockMessage.content = { text: '' };
// ... test implementation
});
it('should handle null values', async () => {
mockMessage.content = null as any;
// ... test implementation
});
it('should handle very long input', async () => {
mockMessage.content = { text: 'a'.repeat(10000) };
// ... test implementation
});
});
// Always await async operations
it('should handle async operations', async () => {
const promise = someAsyncOperation();
await expect(promise).resolves.toBe(expectedValue);
});
// Test rejected promises
it('should handle errors', async () => {
const promise = failingOperation();
await expect(promise).rejects.toThrow('Expected error');
});
// Use async/await instead of .then()
it('should process data', async () => {
const result = await processData();
expect(result).toBeDefined();
});
afterEach(() => {
// Reset all mocks after each test
mock.restore();
// Clean up any side effects
// Clear timers, close connections, etc.
});
// Ensure all code paths are tested
describe('Complete Coverage', () => {
it('should test success path', async () => {
// Test the happy path
});
it('should test error handling', async () => {
// Test error scenarios
});
it('should test edge cases', async () => {
// Test boundary conditions
});
it('should test all conditional branches', async () => {
// Test if/else, switch cases, etc.
});
});
bun test --coverage
to check coverage regularly# Run all tests
bun run test
# Run tests in watch mode
bun run test --watch
# Run specific test file
bun run test src/__tests__/actions.test.ts
# Run tests with coverage
bun run test:coverage
# Run tests matching pattern
bun run test --test-name-pattern "should validate"
bunfig.toml
file in your project root:
[test]
root = "./src/__tests__"
coverage = true
coverageThreshold = 95 # Minimum 95% coverage required, aim for 100%
// Add console.logs for debugging
it('should debug test', async () => {
console.log('Current state:', mockState);
const result = await action.handler(...);
console.log('Result:', result);
console.log('Callback calls:', (callbackFn as any).mock.calls);
});
// Solution: Ensure the mock is set before the action is called
mockRuntime.useModel = mock().mockResolvedValue(response);
// THEN call the action
await action.handler(...);
// Solution: Mock all async dependencies
beforeEach(() => {
// Mock all external calls
mockRuntime.getMemories = mock().mockResolvedValue([]);
mockRuntime.searchMemories = mock().mockResolvedValue([]);
mockRuntime.createMemory = mock().mockResolvedValue({ id: 'test' });
});
// Solution: Reset mocks between tests
afterEach(() => {
mock.restore();
});
// And use fresh setup for each test
beforeEach(() => {
const setup = setupActionTest();
// ... assign to test variables
});
Was this page helpful?