Test Setup
ElizaOS uses Vitest for all tests:# Run all tests
bun test
# Run tests in watch mode
bun test --watch
# Run specific test file
bun test path/to/test.test.ts
# Run tests with coverage
bun test --coverage
Testing Actions
Basic Action Test
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { setupActionTest, cleanupTestRuntime } from './test-utils';
import { myAction } from './my-action';
describe('My Action', () => {
let runtime: IAgentRuntime;
let message: Memory;
let state: State;
let callback: HandlerCallback;
beforeEach(async () => {
const setup = await setupActionTest();
runtime = setup.runtime;
message = setup.message;
state = setup.state;
callback = setup.callback;
});
afterEach(async () => {
await cleanupTestRuntime(runtime);
vi.clearAllMocks();
});
it('should validate correctly', async () => {
const isValid = await myAction.validate(runtime, message, state);
expect(isValid).toBe(true);
});
it('should handle action successfully', async () => {
const result = await myAction.handler(
runtime,
message,
state,
{},
callback
);
expect(result.success).toBe(true);
expect(callback).toHaveBeenCalled();
});
});
Testing with Mocks
Reference: packages/typescript/src/bootstrap/__tests__/actions.test.ts
import { vi } from 'vitest';
it('should call external API', async () => {
// Mock the API call
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ result: 'success' })
});
global.fetch = mockFetch;
await myAction.handler(runtime, message, state, {}, callback);
expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/endpoint',
expect.any(Object)
);
});
it('should use model correctly', async () => {
// Spy on useModel
vi.spyOn(runtime, 'useModel').mockResolvedValue(
'<response><text>Generated response</text></response>'
);
await myAction.handler(runtime, message, state, {}, callback);
expect(runtime.useModel).toHaveBeenCalledWith(
ModelType.TEXT_LARGE,
expect.objectContaining({
prompt: expect.stringContaining('expected prompt content')
})
);
});
Testing Validation
it('should not validate without required permission', async () => {
vi.spyOn(runtime, 'getParticipantUserState').mockResolvedValue('MUTED');
const isValid = await myAction.validate(runtime, message);
expect(isValid).toBe(false);
});
it('should validate with proper context', async () => {
const message = createTestMemory({
content: { text: 'trigger phrase' }
});
const isValid = await myAction.validate(runtime, message);
expect(isValid).toBe(true);
});
Testing Error Handling
it('should handle errors gracefully', async () => {
// Mock a failure
vi.spyOn(runtime, 'useModel').mockRejectedValue(
new Error('API timeout')
);
const result = await myAction.handler(
runtime,
message,
state,
{},
callback
);
expect(result.success).toBe(false);
expect(result.text).toContain('timeout');
});
Testing Providers
import { describe, it, expect } from 'vitest';
import { myProvider } from './my-provider';
describe('My Provider', () => {
it('should return correct data', async () => {
const result = await myProvider.get(runtime, message, state);
expect(result.name).toBe('MY_PROVIDER');
expect(result.data).toBeDefined();
expect(result.text).toContain('expected content');
});
it('should handle missing data', async () => {
// Set up scenario with no data
vi.spyOn(runtime, 'getMemories').mockResolvedValue([]);
const result = await myProvider.get(runtime, message, state);
expect(result.text).toBe('');
});
});
Testing Services
import { describe, it, expect, beforeEach } from 'vitest';
import { MyService } from './my-service';
describe('My Service', () => {
let service: MyService;
let runtime: IAgentRuntime;
beforeEach(async () => {
const setup = await setupActionTest();
runtime = setup.runtime;
service = new MyService(runtime);
await service.initialize();
});
it('should initialize correctly', () => {
expect(service).toBeDefined();
});
it('should perform service operation', async () => {
const result = await service.performOperation('test-data');
expect(result).toBeDefined();
expect(result.success).toBe(true);
});
it('should be accessible from runtime', () => {
const retrievedService = runtime.getService('MyService');
expect(retrievedService).toBe(service);
});
});
Testing Plugins
import { describe, it, expect } from 'vitest';
import { myPlugin } from './my-plugin';
describe('My Plugin', () => {
it('should have correct structure', () => {
expect(myPlugin.name).toBe('my-plugin');
expect(myPlugin.description).toBeTruthy();
expect(Array.isArray(myPlugin.actions)).toBe(true);
});
it('should register with runtime', async () => {
const runtime = new AgentRuntime({
character: testCharacter,
plugins: [myPlugin]
});
// Check if actions are registered
const actions = runtime.getActions();
expect(actions.some(a => a.name === 'MY_ACTION')).toBe(true);
});
it('should initialize services', async () => {
const runtime = new AgentRuntime({
character: testCharacter,
plugins: [myPlugin]
});
const service = runtime.getService('MyService');
expect(service).toBeDefined();
});
});
Test Utilities
ElizaOS provides test utilities to simplify testing:Reference: packages/typescript/src/bootstrap/__tests__/test-utils.ts
import { setupActionTest, createTestMemory, stringToUuid } from './test-utils';
// Create a test runtime with mock data
const setup = await setupActionTest({
messageOverrides: {
content: { text: 'Custom message' }
}
});
// Create test memories
const memory = createTestMemory({
content: {
text: 'Test message',
actions: ['TEST_ACTION']
},
roomId: testRoomId
});
// Generate deterministic UUIDs for testing
const testId = stringToUuid('test-id');
Integration Tests
Test complete workflows:import { describe, it, expect } from 'vitest';
describe('Complete Workflow', () => {
it('should process multi-step action chain', async () => {
const runtime = await createTestRuntime({
plugins: [myPlugin]
});
// Step 1: User message
const userMessage = createTestMemory({
content: { text: 'Start workflow' }
});
// Process message
const response1 = await runtime.processMessage(userMessage);
expect(response1.content.actions).toContain('STEP_1');
// Step 2: Follow-up
const followUp = createTestMemory({
content: { text: 'Continue' }
});
const response2 = await runtime.processMessage(followUp);
expect(response2.content.actions).toContain('STEP_2');
});
});
Testing RAG/Knowledge
import { describe, it, expect, beforeEach } from 'vitest';
describe('Knowledge Retrieval', () => {
let runtime: IAgentRuntime;
beforeEach(async () => {
const setup = await setupActionTest();
runtime = setup.runtime;
// Add test knowledge
await runtime.createMemory({
id: stringToUuid('knowledge-1'),
entityId: runtime.agentId,
agentId: runtime.agentId,
roomId: testRoomId,
content: {
text: 'ElizaOS is a multi-agent framework',
metadata: { type: 'knowledge' }
}
}, 'knowledge');
});
it('should retrieve relevant knowledge', async () => {
const results = await runtime.knowledgeManager.searchMemories({
roomId: testRoomId,
query: 'What is ElizaOS?',
limit: 5
});
expect(results.length).toBeGreaterThan(0);
expect(results[0].content.text).toContain('ElizaOS');
});
});
Testing Multi-Agent Systems
import { describe, it, expect } from 'vitest';
describe('Multi-Agent Communication', () => {
it('should allow agents to communicate', async () => {
const agent1 = await createTestRuntime({ name: 'Agent1' });
const agent2 = await createTestRuntime({ name: 'Agent2' });
// Create shared room
const roomId = stringToUuid('shared-room');
await agent1.ensureRoomExists({
id: roomId,
name: 'Shared Room',
source: 'test'
});
// Agent 1 sends message
const message = createTestMemory({
entityId: agent1.agentId,
roomId: roomId,
content: { text: 'Hello Agent 2' }
});
await agent1.createMemory(message, 'messages');
// Agent 2 retrieves message
const messages = await agent2.getMemories({
roomId: roomId,
type: 'messages',
limit: 1
});
expect(messages[0].content.text).toBe('Hello Agent 2');
});
});
Mocking Best Practices
import { vi } from 'vitest';
import { logger } from '@elizaos/core';
// Mock logger to reduce noise
beforeEach(() => {
vi.spyOn(logger, 'error').mockImplementation(() => {});
vi.spyOn(logger, 'warn').mockImplementation(() => {});
vi.spyOn(logger, 'debug').mockImplementation(() => {});
});
// Mock external services
const mockExternalService = {
fetch: vi.fn().mockResolvedValue({ data: 'test' }),
update: vi.fn().mockResolvedValue({ success: true })
};
// Use in tests
it('should use external service', async () => {
const result = await myAction.handler(
runtime,
message,
state,
{ externalService: mockExternalService },
callback
);
expect(mockExternalService.fetch).toHaveBeenCalled();
});
Coverage Goals
Aim for these coverage targets:- Actions: 80%+ coverage
- Providers: 70%+ coverage
- Services: 75%+ coverage
- Critical paths: 95%+ coverage
- Error handling: Test all error paths
Continuous Integration
Run tests in CI/CD:.github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: oven-sh/setup-bun@v1
- run: bun install
- run: bun test --coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
Test Organization
Organize tests alongside source files:my-plugin/
├── src/
│ ├── actions/
│ │ ├── my-action.ts
│ │ └── __tests__/
│ │ └── my-action.test.ts
│ ├── providers/
│ │ ├── my-provider.ts
│ │ └── __tests__/
│ │ └── my-provider.test.ts
│ └── index.ts
└── package.json
Best Practices
- Write tests before fixing bugs (TDD)
- Test edge cases and error conditions
- Use descriptive test names
- Keep tests focused and independent
- Mock external dependencies
- Use test utilities for common setup
- Clean up resources in
afterEach - Run tests before committing
- Don’t test implementation details
- Don’t skip cleanup in
afterEach - Don’t share state between tests
- Don’t make tests dependent on execution order
- Don’t forget to test error paths
Next Steps
Deployment
Deploy your tested agents to production
Creating Plugins
Build well-tested plugins