@quarry-systems/drift-testing
v0.3.0-alpha.3
Published
Mock services and testing utilities for MCG (Managed Cyclic Graph) workflows.
Readme
@quarry-systems/drift-testing
Mock services and testing utilities for MCG (Managed Cyclic Graph) workflows.
Overview
drift-testing provides a comprehensive mock service framework for testing MCG workflows. It includes:
- Mock Service Base Classes - Extensible base for creating custom mocks
- Pre-built Mock Services - Secrets, HTTP, and more
- Call Tracking - Automatic recording of all method calls
- Fluent API - Chainable methods for easy test setup
- Behavior Configuration - Control return values, errors, and delays
Installation
npm install @quarry-systems/drift-testing --save-devQuick Start
import { mockSecrets, mockHttp } from '@quarry-systems/drift-testing';
import { createManager } from '@quarry-systems/drift-core';
describe('My Workflow', () => {
it('should fetch data with secrets', async () => {
// Create mocks
const secrets = mockSecrets({
'API_KEY': 'test-key-123'
});
const http = mockHttp()
.getReturns('/api/data', { items: [] });
// Use in manager
const manager = createManager(myGraph, {
services: {
secrets: secrets.build(),
http: http.build()
}
});
await manager.start();
// Assert calls
expect(secrets.wasResolveCalledWith('API_KEY')).toBe(true);
expect(http.wasGetCalledWith('/api/data')).toBe(true);
});
});Mock Services
Mock Secrets
import { mockSecrets } from '@quarry-systems/drift-testing';
const secrets = mockSecrets({
'DB_PASSWORD': 'test-password',
'API_KEY': 'test-key'
})
.resolveThrows('INVALID_KEY', new Error('Not found'))
.resolveDelays(100);
const service = secrets.build();
// Use in tests
await service.resolve('DB_PASSWORD'); // Returns 'test-password'
await service.resolve('INVALID_KEY'); // Throws error
// Assert calls
expect(secrets.getResolveCallCount()).toBe(2);
expect(secrets.wasResolveCalledWith('DB_PASSWORD')).toBe(true);Mock HTTP
import { mockHttp } from '@quarry-systems/drift-testing';
const http = mockHttp()
.getReturns('/api/users', { users: [] })
.postReturns('/api/users', { id: 1, name: 'Test' }, 201)
.postThrows('/api/invalid', new Error('Validation failed'))
.withDelay(50);
const service = http.build();
// Use in tests
const response = await service.get('/api/users');
expect(response.status).toBe(200);
// Assert calls
expect(http.getGetCallCount()).toBe(1);
expect(http.wasPostCalledWith('/api/users')).toBe(false);Mock Store
import { mockStore } from '@quarry-systems/drift-testing';
const store = mockStore({
'user:123': { name: 'Alice', email: '[email protected]' },
'config:theme': 'dark'
})
.getThrows('INVALID_KEY', new Error('Key not found'))
.withDelay(10);
const service = store.build();
// Use in tests
const user = await service.get('user:123');
expect(user).toEqual({ name: 'Alice', email: '[email protected]' });
await service.set('session:abc', { userId: '123' });
await service.delete('user:123');
// Assert calls
expect(store.wasGetCalledWith('user:123')).toBe(true);
expect(store.getSetCallCount()).toBe(1);Mock Vectors
import { mockVectors } from '@quarry-systems/drift-testing';
const vectors = mockVectors()
.queryReturns([0.1, 0.2, 0.3], [
{ id: '1', document: 'Relevant document', score: 0.95 },
{ id: '2', document: 'Another match', score: 0.87 }
])
.insertReturns('doc-1', true)
.withDelay(50);
const service = vectors.build();
// Use in tests
const results = await service.query([0.1, 0.2, 0.3], { topK: 5 });
expect(results).toHaveLength(2);
expect(results[0].score).toBe(0.95);
await service.insert('doc-1', [0.1, 0.2, 0.3], 'New document');
// Assert calls
expect(vectors.wasQueryCalledWith([0.1, 0.2, 0.3])).toBe(true);
expect(vectors.getInsertCallCount()).toBe(1);API Reference
MockService Base Class
All mock services extend MockService which provides:
getCalls()- Get all recorded callsgetCallsFor(method)- Get calls for specific methodgetCallCount(method)- Get call count for methodwasCalled(method)- Check if method was calledwasCalledWith(method, ...args)- Check if called with argsclearCalls()- Clear call historyreset()- Reset calls and behaviors
MockBuilder Base Class
All mock builders extend MockBuilder which provides:
build()- Get the mock service instancegetCalls()- Get all recorded callsgetCallsFor(method)- Get calls for specific methodwasCalled(method)- Check if method was calledwasCalledWith(method, ...args)- Check if called with argsreset()- Reset mock state
Creating Custom Mocks
import { MockService, MockBuilder } from '@quarry-systems/drift-testing';
interface MyService {
doSomething(arg: string): Promise<string>;
}
class MockMyService extends MockService implements MyService {
async doSomething(arg: string): Promise<string> {
return this.executeMock('doSomething', [arg], `result-${arg}`);
}
}
class MockMyServiceBuilder extends MockBuilder<MyService> {
constructor() {
const mockService = new MockMyService();
super(mockService, mockService);
}
doSomethingReturns(value: string): this {
this.setBehavior('doSomething', { returns: value });
return this;
}
doSomethingThrows(error: Error): this {
this.setBehavior('doSomething', { throws: error });
return this;
}
}
export function mockMyService(): MockMyServiceBuilder {
return new MockMyServiceBuilder();
}Recording & Replay
Recording Service Calls
Record real service calls during execution for later replay:
import { createRecorder } from '@quarry-systems/drift-testing';
const recorder = createRecorder();
const manager = createManager(graph, {
services: recorder.wrapServices({
secrets: mySecretsService,
http: myHttpService
})
});
await manager.start();
// Get the recording
const recording = recorder.getRecording('run-123', 'graph-456');
// Save to file
fs.writeFileSync('recording.json', recorder.toJSON());Replaying Recorded Calls
Replay recorded calls for deterministic testing:
import { createReplayer, loadRecording } from '@quarry-systems/drift-testing';
// Load recording
const recording = loadRecording(fs.readFileSync('recording.json', 'utf-8'));
const replayer = createReplayer(recording);
const manager = createManager(graph, {
services: replayer.wrapServices({
secrets: mySecretsService,
http: myHttpService
})
});
await manager.start(); // Uses recorded responses
// Validate replay
const validation = replayer.getValidation();
expect(validation.valid).toBe(true);
expect(validation.callsReplayed).toBe(validation.callsExpected);Recording Options
const recorder = createRecorder({
services: ['http'], // Only record specific services
excludeMethods: ['ping'], // Exclude certain methods
maxCalls: 1000, // Limit number of calls
recordErrors: true, // Record errors (default: true)
recordDuration: true // Record call duration (default: true)
});Replay Options
const replayer = createReplayer(recording, {
validateSequence: true, // Validate call order (default: true)
validateArgs: true, // Validate arguments (default: true)
throwOnMismatch: false, // Throw on validation errors (default: false)
services: ['http'] // Only replay specific services
});Recording Format
Recordings are saved as JSON:
{
"metadata": {
"version": "1.0.0",
"timestamp": "2025-12-27T10:00:00Z",
"runId": "run-123",
"graphId": "graph-456",
"totalCalls": 5,
"totalDuration": 1234,
"services": ["secrets", "http"]
},
"calls": [
{
"service": "secrets",
"method": "resolve",
"args": ["API_KEY"],
"result": "test-key-123",
"timestamp": 1735300800000,
"duration": 10,
"sequence": 0
}
]
}Testing Patterns
Error Simulation
const secrets = mockSecrets()
.resolveThrows('MISSING_KEY', new Error('Secret not found'));
await expect(service.resolve('MISSING_KEY')).rejects.toThrow('Secret not found');Delay Simulation
const http = mockHttp()
.withDelay(1000);
const start = Date.now();
await service.get('/api/slow');
const duration = Date.now() - start;
expect(duration).toBeGreaterThan(900);Call Verification
const secrets = mockSecrets({ 'KEY': 'value' });
const service = secrets.build();
await service.resolve('KEY');
await service.resolve('KEY');
expect(secrets.getResolveCallCount()).toBe(2);
expect(secrets.wasResolveCalledWith('KEY')).toBe(true);
const calls = secrets.getCallsFor('resolve');
expect(calls[0].args[0]).toBe('KEY');Record and Replay Workflow
// 1. Record a real execution
const recorder = createRecorder();
const manager = createManager(graph, {
services: recorder.wrapServices({ secrets, http })
});
await manager.start({ query: 'test' });
fs.writeFileSync('test-run.json', recorder.toJSON());
// 2. Replay in tests (fast and deterministic)
const recording = loadRecording(fs.readFileSync('test-run.json', 'utf-8'));
const replayer = createReplayer(recording);
const testManager = createManager(graph, {
services: replayer.wrapServices({ secrets, http })
});
await testManager.start({ query: 'test' }); // Instant, uses recorded data
const validation = replayer.getValidation();
expect(validation.valid).toBe(true);Building
Run nx build drift-testing to build the library.
Running Tests
Run nx test drift-testing or npm test to execute the unit tests via Vitest.
Documentation
For more examples and detailed documentation, see the examples directory.
License
Dual-licensed under AGPL-3.0 and Commercial License. See LICENSE files for details.
