Testing Guide
May 8, 2025 · View on GitHub
This document provides guidelines and best practices for testing the Memory Graph MCP codebase.
Testing Philosophy
The Memory Graph MCP follows these testing principles:
- Test-Driven Development: Write tests before or alongside implementation
- Comprehensive Coverage: Test all core functionality
- Isolation: Unit tests should isolate components from their dependencies
- Realistic Scenarios: Integration tests should model real-world use cases
- Performance Awareness: Test for performance regressions in key operations
Test Types
Unit Tests
Unit tests focus on testing individual components in isolation:
- Test a single function, method, or class
- Mock or stub external dependencies
- Focus on function behavior, not implementation details
- Test edge cases and error conditions
Example unit test:
// MermaidGenerator.test.ts
describe('MermaidGenerator', () => {
test('should generate valid mermaid syntax', () => {
const generator = new MermaidGenerator();
const nodes = new Map([
['node1', { id: 'node1', content: 'Test Node 1', timestamp: '2023-01-01T00:00:00Z' }]
]);
const edges = [
{ source: 'node1', target: 'node2', type: 'relates_to', strength: 0.8, timestamp: '2023-01-01T00:00:00Z' }
];
const result = generator.generateGraph({
startNodeId: 'node1',
nodes,
edges,
direction: 'LR'
});
expect(result).toContain('graph LR');
expect(result).toContain('node1["Test Node 1"]');
});
});
Integration Tests
Integration tests verify the interaction between components:
- Test multiple components working together
- Use real implementations instead of mocks when possible
- Verify system behavior across component boundaries
- Test realistic data flows
Example integration test:
// MemoryGraph.test.ts (integration aspects)
describe('MemoryGraph with storage', () => {
let graph: MemoryGraph;
let tempDir: string;
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'memory-graph-test-'));
graph = new MemoryGraph({ storageDir: tempDir, storageType: 'json' });
await graph.initialize();
});
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
});
test('should persist memories across initialization', async () => {
// Store a memory
const stored = await graph.storeMemory({
content: 'Test memory',
tags: ['test']
});
// Create a new graph instance with the same storage
const newGraph = new MemoryGraph({ storageDir: tempDir, storageType: 'json' });
await newGraph.initialize();
// Recall the memory
const result = await newGraph.recallMemories({
maxNodes: 1,
strategy: 'content',
search: {
keywords: ['Test memory']
}
});
expect(result.nodes).toHaveLength(1);
expect(result.nodes[0].id).toBe(stored.id);
});
});
Storage Backend Tests
Storage backend tests verify each storage implementation:
- Test all storage operations for each implementation
- Verify data consistency and integrity
- Test error handling and edge cases
- Verify full-text search capabilities
Example storage test:
// SqliteMemoryStorage.test.ts
describe('SqliteMemoryStorage', () => {
let storage: SqliteMemoryStorage;
let tempDir: string;
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'sqlite-test-'));
storage = new SqliteMemoryStorage(tempDir);
await storage.initialize();
});
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
});
test('should search content with FTS', async () => {
// Create test domain
const domain: DomainInfo = {
id: 'test',
name: 'Test Domain',
description: 'Test domain for SQLite search',
created: new Date().toISOString(),
lastAccess: new Date().toISOString()
};
await storage.createDomain(domain);
// Create test memories
const nodes = new Map<string, MemoryNode>();
nodes.set('node1', {
id: 'node1',
content: 'Architecture decisions for microservices',
timestamp: new Date().toISOString()
});
nodes.set('node2', {
id: 'node2',
content: 'Deployment strategy for AWS',
timestamp: new Date().toISOString()
});
await storage.saveMemories('test', nodes, []);
// Test search
const results = await storage.searchContent('microservices architecture');
expect(results).toHaveLength(1);
expect(results[0].id).toBe('node1');
});
});
Tool API Tests
Tool API tests verify the MCP tool implementations:
- Test each tool's handling of various inputs
- Verify expected responses and error handling
- Test input validation and edge cases
- Simulate MCP client interaction
Example tool test:
// memoryTools.test.ts
describe('recall_memories tool', () => {
let memoryTools: MemoryTools;
let mockGraph: jest.Mocked<MemoryGraphInterface>;
beforeEach(() => {
mockGraph = {
recallMemories: jest.fn(),
storeMemory: jest.fn(),
// ... other methods
} as any;
memoryTools = new MemoryTools(mockGraph);
});
test('should handle recall_memories correctly', async () => {
// Mock the graph response
mockGraph.recallMemories.mockResolvedValue({
nodes: [{
id: 'node1',
content: 'Test memory',
timestamp: '2023-01-01T00:00:00Z'
}]
});
// Call the tool
const result = await memoryTools.handleToolCall({
name: 'recall_memories',
input: {
maxNodes: 10,
strategy: 'recent'
}
});
// Verify result
expect(mockGraph.recallMemories).toHaveBeenCalledWith({
maxNodes: 10,
strategy: 'recent'
});
expect(result.content[0].type).toBe('text');
expect(JSON.parse(result.content[0].text).nodes).toHaveLength(1);
});
});
Performance Tests
Performance tests identify potential performance issues:
- Test operations with large datasets
- Measure execution time for key operations
- Compare different storage backends
- Establish performance baselines
Example performance test:
// performance.test.ts
describe('Performance tests', () => {
// These tests might be skipped in regular CI runs
test.skip('should handle large memory sets efficiently', async () => {
const graph = new MemoryGraph({
storageDir: './perf_test',
storageType: 'sqlite'
});
await graph.initialize();
// Create many memories
const startTime = Date.now();
for (let i = 0; i < 1000; i++) {
await graph.storeMemory({
content: `Test memory ${i}`,
tags: ['performance', 'test']
});
}
const createTime = Date.now() - startTime;
// Recall with various strategies
const recallTime = await measureRecallPerformance(graph);
// Report results
console.log(`Created 1000 memories in ${createTime}ms`);
console.log(`Average recall time: ${recallTime}ms`);
// Cleanup
// ...
// Soft assertions for CI
expect(createTime).toBeLessThan(30000); // 30 seconds
expect(recallTime).toBeLessThan(500); // 500ms
});
});
Test Setup
Directory and File Structure
memory-graph/
├── src/
│ ├── component/
│ │ ├── Component.ts
│ │ └── Component.test.ts // Unit tests alongside code
├── tests/
│ ├── integration/ // Integration tests
│ ├── performance/ // Performance tests
│ └── utils/ // Test utilities
Testing Environment Setup
Tests use these environment settings:
- Temporary directories for storage tests
- In-memory SQLite for database tests
- Mock database for MariaDB tests
- Isolated domains for multi-domain tests
Test Implementation Best Practices
Writing Effective Tests
-
Test Organization:
- Group related tests with
describeblocks - Use descriptive test names with
testorit - Structure tests as Arrange-Act-Assert (AAA)
- Group related tests with
-
Effective Assertions:
- Test for the expected result, not implementation details
- Be specific about what you're testing
- Include helpful error messages on failures
-
Test Isolation:
- Reset state between tests
- Don't rely on test execution order
- Clean up resources (like temp files) after tests
-
Test Coverage:
- Test happy paths, edge cases, and error conditions
- Verify data integrity and consistency
- Test asynchronous behavior correctly
Test Data Management
Managing test data effectively:
// Creating test domains
async function createTestDomain(graph: MemoryGraph, id: string): Promise<DomainInfo> {
return await graph.createDomain(
id,
`Test Domain ${id}`,
`Test domain for ${id}`
);
}
// Creating test memory nodes
async function createTestMemories(graph: MemoryGraph, count: number): Promise<MemoryNode[]> {
const nodes: MemoryNode[] = [];
for (let i = 0; i < count; i++) {
const node = await graph.storeMemory({
content: `Test memory ${i}`,
tags: [`tag${i % 3}`],
path: `/test/path${i % 5}`
});
nodes.push(node);
}
return nodes;
}
// Creating test relationships
async function createTestRelationships(graph: MemoryGraph, nodes: MemoryNode[]): Promise<void> {
for (let i = 0; i < nodes.length - 1; i++) {
await graph.editMemory({
id: nodes[i].id,
relationships: {
relates_to: [
{ targetId: nodes[i + 1].id, strength: 0.8 }
]
}
});
}
}
Mocking and Stubbing
Techniques for isolating components:
// Mocking the storage layer
jest.mock('../../src/storage/JsonMemoryStorage', () => {
return {
JsonMemoryStorage: jest.fn().mockImplementation(() => ({
initialize: jest.fn().mockResolvedValue(undefined),
getDomains: jest.fn().mockResolvedValue(new Map()),
saveDomains: jest.fn().mockResolvedValue(undefined),
// ... other methods
}))
};
});
// Stubbing implementation
const storageStub = {
initialize: jest.fn().mockResolvedValue(undefined),
getMemories: jest.fn().mockResolvedValue({
nodes: new Map([
['node1', { id: 'node1', content: 'Test', timestamp: new Date().toISOString() }]
]),
edges: []
}),
// ... other methods
};
Running Tests
Basic Test Commands
# Run all tests
npm test
# Run with watch mode (for development)
npm run test:watch
# Run with coverage report
npm run test:coverage
Running Specific Tests
# Run tests for a specific file
npm test -- src/graph/MemoryGraph.test.ts
# Run tests matching a pattern
npm test -- -t "should store memory"
# Run a specific test suite
npm test -- -t "MemoryGraph"
CI Integration
Tests run in CI environments with these considerations:
- All tests must pass for PR approval
- Coverage thresholds are enforced
- Performance tests may be skipped or run separately
- Test databases are created and torn down for each run
Troubleshooting Tests
Common Issues
-
Test Data Persistence:
- Tests may fail if data from previous tests isn't cleaned up
- Use unique test directories for storage tests
- Clean up in
afterEachorafterAllblocks
-
Asynchronous Testing:
- Ensure proper use of
async/awaitin tests - Use appropriate Jest matchers for Promises
- Watch for unhandled promise rejections
- Ensure proper use of
-
Test Isolation:
- Avoid shared state between tests
- Reset mocks between tests with
jest.resetAllMocks() - Create fresh instances in
beforeEach
-
Database Tests:
- Use in-memory databases when possible
- Handle connection pooling properly
- Ensure database schemas are created correctly
Debugging Tests
Techniques for debugging test failures:
# Run with verbose output
npm test -- --verbose
# Run with debugging
NODE_OPTIONS=--inspect-brk npm test -- --runInBand
# Add debug logging (outputs to stderr)
console.error('Debug:', value);
Test Coverage
The project aims for high test coverage:
# Generate coverage report
npm run test:coverage
Coverage targets:
- Lines: >85%
- Functions: >90%
- Branches: >75%
- Statements: >85%