Plugin Testing Guide
Comprehensive testing strategies and best practices for ElizaOS plugins
This guide covers comprehensive testing strategies for ElizaOS plugins, including unit tests, integration tests, and end-to-end testing approaches.
Testing Overview
ElizaOS plugins support multiple testing approaches:
- Unit Tests: Test individual components in isolation
- Integration Tests: Test component interactions
- End-to-End Tests: Test complete workflows with real runtime
- Component Tests: Test specific plugin components
- Performance Tests: Test plugin performance and resource usage
Test Setup
Project Structure
plugin-name/
├── src/
│ ├── __tests__/
│ │ ├── unit/
│ │ │ ├── actions.test.ts
│ │ │ ├── providers.test.ts
│ │ │ └── services.test.ts
│ │ ├── integration/
│ │ │ ├── plugin.test.ts
│ │ │ └── workflow.test.ts
│ │ └── e2e/
│ │ ├── plugin-e2e.test.ts
│ │ └── scenarios.test.ts
│ └── test-utils/
│ ├── mocks.ts
│ ├── fixtures.ts
│ └── helpers.ts
├── vitest.config.ts
└── package.json
Configuration
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "node",
globals: true,
setupFiles: ["./src/test-utils/setup.ts"],
},
});
Test Dependencies
{
"devDependencies": {
"vitest": "^1.0.0",
"@vitest/ui": "^1.0.0",
"vitest-mock-extended": "^1.0.0",
"@elizaos/core": "workspace:*"
}
}
Unit Testing
Testing Actions
// src/__tests__/unit/actions.test.ts
import { IAgentRuntime, Memory, State } from "@elizaos/core";
import { describe, expect, it, vi } from "vitest";
import { greetAction } from "../../actions/greetAction";
describe("Greet Action", () => {
const mockRuntime = {
getSetting: vi.fn(),
getService: vi.fn(),
} as unknown as IAgentRuntime;
const mockMessage: Memory = {
content: { text: "hello" },
userId: "test-user",
roomId: "test-room",
};
const mockState: State = {
userId: "test-user",
roomId: "test-room",
agentId: "test-agent",
};
it("should validate correctly for greeting messages", async () => {
const result = await greetAction.validate(mockRuntime, mockMessage);
expect(result).toBe(true);
});
it("should not validate for non-greeting messages", async () => {
const nonGreetingMessage = {
...mockMessage,
content: { text: "what is the weather?" },
};
const result = await greetAction.validate(mockRuntime, nonGreetingMessage);
expect(result).toBe(false);
});
it("should handle greeting correctly", async () => {
const result = await greetAction.handler(mockRuntime, mockMessage, mockState);
expect(result).toBeDefined();
expect(result.text).toContain("Hello");
expect(result.actions).toContain("GREET");
});
it("should handle missing userId gracefully", async () => {
const messageWithoutUser = {
...mockMessage,
userId: undefined,
};
const result = await greetAction.handler(mockRuntime, messageWithoutUser, mockState);
expect(result.text).toContain("friend");
});
});
Testing Providers
// src/__tests__/unit/providers.test.ts
import { IAgentRuntime, Memory, State } from "@elizaos/core";
import { describe, expect, it, vi } from "vitest";
import { weatherProvider } from "../../providers/weatherProvider";
describe("Weather Provider", () => {
const mockRuntime = {
getSetting: vi.fn(),
} as unknown as IAgentRuntime;
const mockMessage: Memory = {
content: { text: "what is the weather in New York?" },
userId: "test-user",
roomId: "test-room",
};
const mockState: State = {
userId: "test-user",
roomId: "test-room",
agentId: "test-agent",
};
it("should provide weather data", async () => {
const result = await weatherProvider.get(mockRuntime, mockMessage, mockState);
expect(result).toBeDefined();
expect(result.text).toContain("weather");
expect(result.values).toHaveProperty("location");
expect(result.values).toHaveProperty("temperature");
});
it("should extract location from message", async () => {
const result = await weatherProvider.get(mockRuntime, mockMessage, mockState);
expect(result.values.location).toBe("New York");
});
it("should handle missing location gracefully", async () => {
const messageWithoutLocation = {
...mockMessage,
content: { text: "what is the weather?" },
};
const result = await weatherProvider.get(mockRuntime, messageWithoutLocation, mockState);
expect(result.values.location).toBe("New York"); // Default location
});
});
Testing Services
// src/__tests__/unit/services.test.ts
import { IAgentRuntime } from "@elizaos/core";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { NotificationService } from "../../services/NotificationService";
describe("Notification Service", () => {
let mockRuntime: IAgentRuntime;
let service: NotificationService;
beforeEach(() => {
mockRuntime = {
getSetting: vi.fn(),
getService: vi.fn(),
} as unknown as IAgentRuntime;
});
it("should start successfully", async () => {
service = await NotificationService.start(mockRuntime);
expect(service).toBeInstanceOf(NotificationService);
expect(service.capabilityDescription).toBeDefined();
});
it("should stop successfully", async () => {
service = await NotificationService.start(mockRuntime);
await expect(service.stop()).resolves.not.toThrow();
});
it("should handle service lifecycle", async () => {
// Test static methods
mockRuntime.getService = vi.fn().mockReturnValue(service);
await expect(NotificationService.stop(mockRuntime)).resolves.not.toThrow();
});
});
Integration Testing
Testing Plugin Integration
// src/__tests__/integration/plugin.test.ts
import { IAgentRuntime } from "@elizaos/core";
import { beforeEach, describe, expect, it } from "vitest";
import { weatherPlugin } from "../../index";
import { createMockRuntime } from "../../test-utils/mocks";
describe("Weather Plugin Integration", () => {
let mockRuntime: IAgentRuntime;
beforeEach(() => {
mockRuntime = createMockRuntime();
});
it("should initialize plugin correctly", async () => {
const config = {
WEATHER_API_KEY: "test-key",
};
await expect(weatherPlugin.init(config, mockRuntime)).resolves.not.toThrow();
});
it("should register all components", () => {
expect(weatherPlugin.actions).toBeDefined();
expect(weatherPlugin.providers).toBeDefined();
expect(weatherPlugin.actions.length).toBeGreaterThan(0);
expect(weatherPlugin.providers.length).toBeGreaterThan(0);
});
it("should handle action-provider interaction", async () => {
// Test interaction between action and provider
const action = weatherPlugin.actions[0];
const provider = weatherPlugin.providers[0];
const mockMessage = {
content: { text: "what is the weather in Paris?" },
userId: "test-user",
roomId: "test-room",
};
const mockState = {
userId: "test-user",
roomId: "test-room",
agentId: "test-agent",
};
// Test that action can validate and use provider data
const isValid = await action.validate(mockRuntime, mockMessage);
expect(isValid).toBe(true);
const providerData = await provider.get(mockRuntime, mockMessage, mockState);
expect(providerData).toBeDefined();
expect(providerData.values.location).toBe("Paris");
});
});
Testing Workflow Integration
// src/__tests__/integration/workflow.test.ts
import { describe, expect, it } from "vitest";
import { weatherPlugin } from "../../index";
import { createMockRuntime } from "../../test-utils/mocks";
describe("Weather Plugin Workflow", () => {
it("should complete full weather query workflow", async () => {
const mockRuntime = createMockRuntime();
// Initialize plugin
await weatherPlugin.init(
{
WEATHER_API_KEY: "test-key",
},
mockRuntime
);
// Simulate message processing
const message = {
content: { text: "what is the weather in London?" },
userId: "test-user",
roomId: "test-room",
};
const state = {
userId: "test-user",
roomId: "test-room",
agentId: "test-agent",
};
// Test provider
const provider = weatherPlugin.providers[0];
const providerResult = await provider.get(mockRuntime, message, state);
expect(providerResult.values.location).toBe("London");
expect(providerResult.values.temperature).toBeDefined();
// Test action
const action = weatherPlugin.actions[0];
const actionResult = await action.handler(mockRuntime, message, state);
expect(actionResult.text).toContain("weather");
expect(actionResult.text).toContain("London");
});
});
End-to-End Testing
E2E Test Suite
// src/__tests__/e2e/plugin-e2e.test.ts
import { TestSuite } from "@elizaos/core";
export const WeatherPluginE2ETestSuite: TestSuite = {
name: "weather_plugin_e2e_test_suite",
description: "End-to-end tests for weather plugin",
tests: [
{
name: "weather_query_workflow",
fn: async (runtime) => {
// Test complete weather query workflow
const message = {
content: { text: "what is the weather in Tokyo?" },
userId: "test-user",
roomId: "test-room",
};
const state = {
userId: "test-user",
roomId: "test-room",
agentId: "test-agent",
};
// Find weather action
const weatherAction = runtime.actions?.find((a) => a.name === "WEATHER_QUERY");
expect(weatherAction).toBeDefined();
// Validate action
const isValid = await weatherAction.validate(runtime, message);
expect(isValid).toBe(true);
// Execute action
const result = await weatherAction.handler(runtime, message, state);
expect(result).toBeDefined();
expect(result.text).toContain("Tokyo");
expect(result.text).toContain("weather");
},
},
{
name: "weather_service_integration",
fn: async (runtime) => {
// Test weather service integration
const weatherService = runtime.getService("weather-service");
expect(weatherService).toBeDefined();
// Test service capabilities
expect(weatherService.capabilityDescription).toContain("weather");
},
},
{
name: "error_handling",
fn: async (runtime) => {
// Test error handling for invalid location
const message = {
content: { text: "what is the weather in InvalidLocation123?" },
userId: "test-user",
roomId: "test-room",
};
const state = {
userId: "test-user",
roomId: "test-room",
agentId: "test-agent",
};
const weatherAction = runtime.actions?.find((a) => a.name === "WEATHER_QUERY");
const result = await weatherAction.handler(runtime, message, state);
// Should handle error gracefully
expect(result.text).toContain("unable") || expect(result.text).toContain("error");
},
},
],
};
Including E2E Tests in Plugin
// src/index.ts
import { Plugin } from "@elizaos/core";
import { WeatherPluginE2ETestSuite } from "./__tests__/e2e/plugin-e2e.test";
export const weatherPlugin: Plugin = {
name: "weather-plugin",
description: "Provides weather information",
// Include E2E tests
tests: [WeatherPluginE2ETestSuite],
// ... other plugin components
};
Test Utilities
Mock Runtime
// src/test-utils/mocks.ts
import { IAgentRuntime } from "@elizaos/core";
import { vi } from "vitest";
export function createMockRuntime(): IAgentRuntime {
return {
getSetting: vi.fn().mockImplementation((key: string) => {
const settings = {
WEATHER_API_KEY: "test-api-key",
OPENAI_API_KEY: "test-openai-key",
};
return settings[key];
}),
getService: vi.fn().mockImplementation((type: string) => {
const services = {
"weather-service": {
capabilityDescription: "Provides weather data",
fetchWeather: vi.fn().mockResolvedValue({
temperature: 72,
conditions: "Sunny",
}),
},
};
return services[type];
}),
actions: [],
providers: [],
evaluators: [],
// Add other runtime methods as needed
} as unknown as IAgentRuntime;
}
Test Fixtures
// src/test-utils/fixtures.ts
import { Memory, State } from "@elizaos/core";
export const mockMessage: Memory = {
content: { text: "test message" },
userId: "test-user",
roomId: "test-room",
agentId: "test-agent",
createdAt: Date.now(),
};
export const mockState: State = {
userId: "test-user",
roomId: "test-room",
agentId: "test-agent",
};
export const weatherMessages = {
simple: {
...mockMessage,
content: { text: "what is the weather?" },
},
withLocation: {
...mockMessage,
content: { text: "what is the weather in New York?" },
},
greeting: {
...mockMessage,
content: { text: "hello" },
},
};
Test Helpers
// src/test-utils/helpers.ts
import { IAgentRuntime, Memory, State } from "@elizaos/core";
export async function simulateMessageProcessing(
runtime: IAgentRuntime,
message: Memory,
state: State
) {
// Simulate the message processing flow
const applicableActions = [];
for (const action of runtime.actions) {
const isValid = await action.validate(runtime, message);
if (isValid) {
applicableActions.push(action);
}
}
// Execute first applicable action
if (applicableActions.length > 0) {
return await applicableActions[0].handler(runtime, message, state);
}
return null;
}
export function createTestMessage(text: string, userId = "test-user"): Memory {
return {
content: { text },
userId,
roomId: "test-room",
agentId: "test-agent",
createdAt: Date.now(),
};
}
Running Tests
CLI Commands
# Run all tests
elizaos test
# Run specific test types
elizaos test component
elizaos test integration
elizaos test e2e
# Run tests with coverage
elizaos test --coverage
# Run tests in watch mode
elizaos test --watch
# Run specific test file
elizaos test weather.test.ts
# Run tests with name filter
elizaos test --name "weather"
Continuous Integration
# .github/workflows/test.yml
name: Test Plugin
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v1
- name: Install dependencies
run: bun install
- name: Run tests
run: bun test
- name: Run E2E tests
run: elizaos test e2e
- name: Check coverage
run: bun test --coverage
Best Practices
Test Organization
- Separate test types into different directories
- Use descriptive test names that explain what is being tested
- Group related tests using describe blocks
- Use beforeEach/afterEach for setup and cleanup
- Keep tests focused on single functionality
Mocking Strategy
- Mock external dependencies (APIs, databases, file system)
- Use test doubles for complex runtime interactions
- Avoid mocking internal plugin logic
- Mock at the boundary between your plugin and external systems
Error Testing
- Test error conditions explicitly
- Verify error messages are user-friendly
- Test recovery mechanisms
- Ensure graceful degradation
Performance Testing
// Performance test example
it("should handle large number of requests efficiently", async () => {
const start = performance.now();
const promises = Array.from({ length: 100 }, (_, i) =>
weatherAction.handler(mockRuntime, createTestMessage(`weather ${i}`), mockState)
);
await Promise.all(promises);
const end = performance.now();
const duration = end - start;
expect(duration).toBeLessThan(5000); // Should complete within 5 seconds
});
Test Coverage
Aim for:
- 90%+ code coverage for critical paths
- 100% coverage for error handling
- Test all public APIs
- Test edge cases and boundary conditions
This comprehensive testing approach ensures your ElizaOS plugins are reliable, maintainable, and production-ready.