Last Updated: January 2025
Status: Comprehensive Testing Documentation
This guide provides comprehensive testing strategies, examples, and best practices for the workflow system. The workflow engine has been enhanced with production-ready features across three phases:
- Phase 1: Critical Fixes (Security, Data Integrity, Error Handling)
- Phase 2: Stabilization (Logging, Metrics, Circuit Breakers)
- Phase 3: Optimization (Parallel Execution, Caching, Rate Limiting)
- Test Structure
- Unit Tests
- Integration Tests
- Performance Tests
- End-to-End Tests
- Testing Best Practices
- Test Coverage Requirements
src/workflows/
├── workflow-engine.service.spec.ts # Main workflow engine tests
├── workflow-engine-optimization.spec.ts # Phase 3 optimization tests
├── workflow-cache.service.spec.ts # Caching service tests ✅
├── workflow-rate-limiter.service.spec.ts # Rate limiting tests ✅
├── workflow-metrics.service.spec.ts # Metrics service tests ✅
├── workflow-parallel-executor.spec.ts # Parallel execution tests ✅
├── workflow-scheduler.service.spec.ts # Scheduler tests
└── workflow-ai-helper.spec.ts # AI helper tests
Status: ✅ Test files created for Phase 3 optimizations
- Unit Tests - Test individual components in isolation
- Integration Tests - Test component interactions
- Performance Tests - Test optimization features
- E2E Tests - Test complete workflow execution
File: workflow-cache.service.spec.ts
Test Cases:
- ✅ Cache workflow definitions
- ✅ Cache AI responses
- ✅ TTL expiration
- ✅ Cache key generation
- ✅ Cache statistics
- ✅ Expired entry cleanup
Example:
describe('WorkflowCacheService', () => {
it('should cache workflow definition', () => {
service.setWorkflow('test-workflow', mockWorkflow);
const cached = service.getWorkflow('test-workflow');
expect(cached).toEqual(mockWorkflow);
});
it('should expire cached workflow after TTL', async () => {
service.setWorkflow('test-workflow', mockWorkflow, 100);
await new Promise(resolve => setTimeout(resolve, 150));
expect(service.getWorkflow('test-workflow')).toBeNull();
});
});File: workflow-rate-limiter.service.spec.ts
Test Cases:
- ✅ Allow requests within limit
- ✅ Reject requests exceeding limit
- ✅ Reset after window expires
- ✅ Track remaining requests
- ✅ Key generation
- ✅ Expired entry cleanup
Example:
describe('WorkflowRateLimiterService', () => {
it('should reject requests exceeding limit', async () => {
const key = 'test-key';
await service.checkRateLimit(key, 2, 60);
await service.checkRateLimit(key, 2, 60);
const result = await service.checkRateLimit(key, 2, 60);
expect(result.allowed).toBe(false);
});
});File: workflow-parallel-executor.spec.ts
Test Cases:
- ✅ Build dependency graph
- ✅ Topological sort
- ✅ Parallel execution groups
- ✅ Circular dependency detection
- ✅ Complex dependency graphs
Example:
describe('topologicalSort', () => {
it('should sort steps with parallel branches', () => {
const steps = [
{ id: 'step1', type: 'CREATE_LEASE' },
{ id: 'step2', type: 'SEND_EMAIL', dependsOn: ['step1'] },
{ id: 'step3', type: 'SCHEDULE_INSPECTION', dependsOn: ['step1'] },
];
const graph = buildStepGraph(steps);
const executionGroups = topologicalSort(graph);
expect(executionGroups[1].length).toBe(2); // step2 and step3 in parallel
});
});File: workflow-engine-optimization.spec.ts
Test Cases:
- ✅ Workflow definition caching
- ✅ AI response caching
- ✅ Rate limiting enforcement
- ✅ Parallel step execution
- ✅ Query optimizations (N+1 fixes)
Example:
it('should execute independent steps in parallel', async () => {
const executionTimes: number[] = [];
service.registerWorkflow({
id: 'parallel-test',
steps: [
{
id: 'step1',
type: 'CUSTOM',
handler: async () => {
const start = Date.now();
await delay(100);
executionTimes.push(Date.now() - start);
return { result: 'step1' };
},
},
{
id: 'step2',
type: 'CUSTOM',
handler: async () => {
const start = Date.now();
await delay(100);
executionTimes.push(Date.now() - start);
return { result: 'step2' };
},
},
],
});
const startTime = Date.now();
await service.executeWorkflow('parallel-test', {}, userId);
const totalDuration = Date.now() - startTime;
// Both steps should start around the same time
expect(Math.abs(executionTimes[0] - executionTimes[1])).toBeLessThan(50);
// Total duration should be ~100ms (parallel) not ~200ms (sequential)
expect(totalDuration).toBeLessThan(150);
});File: workflow-metrics.service.spec.ts
Test Cases:
- ✅ Metric recording
- ✅ Workflow metrics calculation
- ✅ Time window filtering
- ✅ Overall health assessment
- ✅ Alert generation
- ✅ Cache management
Example:
it('should calculate correct metrics', () => {
service.recordMetric({
workflowId: 'test-workflow',
executionId: 'exec-1',
status: 'COMPLETED',
duration: 1000,
stepCount: 3,
failedStepCount: 0,
});
const metrics = service.getWorkflowMetrics('test-workflow', 60);
expect(metrics.totalExecutions).toBe(1);
expect(metrics.successfulExecutions).toBe(1);
expect(metrics.errorRate).toBe(0);
});Test: Execute a full workflow with all features
describe('Workflow Execution Integration', () => {
it('should execute workflow with caching and rate limiting', async () => {
// Setup
const workflow = {
id: 'test-workflow',
name: 'Test',
steps: [
{ id: 'step1', type: 'CREATE_LEASE' },
{ id: 'step2', type: 'SEND_EMAIL', dependsOn: ['step1'] },
],
};
service.registerWorkflow(workflow);
// Execute
const result = await service.executeWorkflow('test-workflow', {}, userId);
// Verify
expect(result.status).toBe('COMPLETED');
expect(result.steps).toHaveLength(2);
expect(cacheService.getWorkflow).toHaveBeenCalled();
expect(rateLimiter.checkRateLimit).toHaveBeenCalled();
});
});Test: Verify parallel steps execute concurrently
it('should execute independent steps in parallel', async () => {
const executionTimes: number[] = [];
service.registerWorkflow({
id: 'parallel-test',
steps: [
{
id: 'step1',
type: 'CUSTOM',
handler: async () => {
const start = Date.now();
await delay(100);
executionTimes.push(Date.now() - start);
return { result: 'step1' };
},
},
{
id: 'step2',
type: 'CUSTOM',
handler: async () => {
const start = Date.now();
await delay(100);
executionTimes.push(Date.now() - start);
return { result: 'step2' };
},
},
],
});
const startTime = Date.now();
await service.executeWorkflow('parallel-test', {}, userId);
const totalDuration = Date.now() - startTime;
// Both steps should start around the same time
expect(Math.abs(executionTimes[0] - executionTimes[1])).toBeLessThan(50);
// Total duration should be ~100ms (parallel) not ~200ms (sequential)
expect(totalDuration).toBeLessThan(150);
});Test: Verify error handling across all layers
it('should handle errors with retry, circuit breaker, and DLQ', async () => {
// Mock AI service to fail
aiService.assignPriorityWithAI.mockRejectedValue(new Error('Service unavailable'));
const result = await service.executeWorkflow('test-workflow', {}, userId);
// Should retry
expect(aiService.assignPriorityWithAI).toHaveBeenCalledTimes(3);
// Should eventually fail and go to DLQ
expect(result.status).toBe('FAILED');
expect(prisma.deadLetterQueue.create).toHaveBeenCalled();
});Test: Verify cache improves performance
describe('Caching Performance', () => {
it('should improve workflow lookup performance', async () => {
// First call (cache miss)
const start1 = Date.now();
await service.executeWorkflow('test-workflow', {}, userId);
const duration1 = Date.now() - start1;
// Second call (cache hit)
const start2 = Date.now();
await service.executeWorkflow('test-workflow', {}, userId);
const duration2 = Date.now() - start2;
// Cached call should be significantly faster
expect(duration2).toBeLessThan(duration1 * 0.5);
});
it('should reduce AI API calls with caching', async () => {
const cacheKey = 'ai:test:method:params';
// First call
await service.executeWorkflow('test-workflow', { title: 'Test' }, userId);
expect(aiService.assignPriorityWithAI).toHaveBeenCalledTimes(1);
// Second call with same params (should use cache)
await service.executeWorkflow('test-workflow', { title: 'Test' }, userId);
expect(aiService.assignPriorityWithAI).toHaveBeenCalledTimes(1); // Still 1, not 2
});
});Test: Verify parallel execution improves performance
describe('Parallel Execution Performance', () => {
it('should execute parallel steps faster than sequential', async () => {
// Sequential workflow
const sequentialWorkflow = {
id: 'sequential',
steps: [
{ id: 's1', type: 'CUSTOM', handler: () => delay(100) },
{ id: 's2', type: 'CUSTOM', dependsOn: ['s1'], handler: () => delay(100) },
{ id: 's3', type: 'CUSTOM', dependsOn: ['s2'], handler: () => delay(100) },
],
};
// Parallel workflow
const parallelWorkflow = {
id: 'parallel',
steps: [
{ id: 'p1', type: 'CUSTOM', handler: () => delay(100) },
{ id: 'p2', type: 'CUSTOM', dependsOn: ['p1'], handler: () => delay(100) },
{ id: 'p3', type: 'CUSTOM', dependsOn: ['p1'], handler: () => delay(100) },
],
};
const sequentialTime = await measureExecution(sequentialWorkflow);
const parallelTime = await measureExecution(parallelWorkflow);
// Parallel should be faster (2 groups vs 3 groups)
expect(parallelTime).toBeLessThan(sequentialTime);
});
});Test: Verify N+1 query fixes
describe('Query Optimization', () => {
it('should use single query instead of N+1', async () => {
await service.executeWorkflow('test-workflow', { requestId: 1 }, userId);
// Should be 1 query with includes, not multiple queries
expect(prisma.maintenanceRequest.findUnique).toHaveBeenCalledTimes(1);
expect(prisma.property.findUnique).not.toHaveBeenCalled();
expect(prisma.asset.findUnique).not.toHaveBeenCalled();
});
});describe('E2E: Complete Workflow Lifecycle', () => {
it('should execute maintenance request workflow end-to-end', async () => {
// 1. Register workflow
service.registerWorkflow({
id: 'maintenance-lifecycle',
steps: [
{ id: 'create', type: 'CREATE_MAINTENANCE_REQUEST' },
{ id: 'assign-priority', type: 'ASSIGN_PRIORITY_AI', dependsOn: ['create'] },
{ id: 'assign-technician', type: 'ASSIGN_TECHNICIAN', dependsOn: ['assign-priority'] },
{ id: 'notify', type: 'SEND_NOTIFICATION', dependsOn: ['assign-technician'] },
],
});
// 2. Execute workflow
const result = await service.executeWorkflow(
'maintenance-lifecycle',
{ title: 'Leak in bathroom', description: 'Water leaking from ceiling' },
userId
);
// 3. Verify execution
expect(result.status).toBe('COMPLETED');
expect(result.steps).toHaveLength(4);
expect(result.steps[0].status).toBe('COMPLETED');
expect(result.steps[1].status).toBe('COMPLETED');
expect(result.steps[2].status).toBe('COMPLETED');
expect(result.steps[3].status).toBe('COMPLETED');
// 4. Verify database persistence
const persisted = await prisma.workflowExecution.findUnique({
where: { id: result.id },
include: { steps: true },
});
expect(persisted).toBeDefined();
expect(persisted.steps).toHaveLength(4);
// 5. Verify metrics
const metrics = metricsService.getWorkflowMetrics('maintenance-lifecycle', 60);
expect(metrics.totalExecutions).toBeGreaterThan(0);
expect(metrics.successfulExecutions).toBeGreaterThan(0);
});
});it('should recover from failures with retry and DLQ', async () => {
// Mock transient failure
let attemptCount = 0;
aiService.assignPriorityWithAI.mockImplementation(async () => {
attemptCount++;
if (attemptCount < 3) {
throw new Error('Temporary failure');
}
return 'HIGH';
});
const result = await service.executeWorkflow('test-workflow', {}, userId);
// Should eventually succeed after retries
expect(result.status).toBe('COMPLETED');
expect(attemptCount).toBe(3);
// If it still fails, should go to DLQ
aiService.assignPriorityWithAI.mockRejectedValue(new Error('Permanent failure'));
const failedResult = await service.executeWorkflow('test-workflow', {}, userId);
expect(failedResult.status).toBe('FAILED');
expect(prisma.deadLetterQueue.create).toHaveBeenCalled();
});- Each test should be independent
- Use
beforeEachto reset state - Mock external dependencies
- Clean up after tests
// Use factories for test data
const createMockWorkflow = (overrides = {}) => ({
id: 'test-workflow',
name: 'Test',
description: 'Test',
steps: [],
...overrides,
});
const createMockExecution = (overrides = {}) => ({
id: 'exec-123',
workflowId: 'test-workflow',
status: 'RUNNING',
input: {},
output: {},
steps: [],
startedAt: new Date(),
completedAt: null,
error: null,
...overrides,
});// Always await async operations
it('should handle async operations', async () => {
const result = await service.executeWorkflow('test', {}, userId);
expect(result).toBeDefined();
});
// Use proper timeout for slow operations
it('should complete within timeout', async () => {
const result = await service.executeWorkflow('test', {}, userId);
expect(result).toBeDefined();
}, 10000); // 10 second timeout// Mock Prisma
const mockPrisma = {
workflowExecution: {
upsert: jest.fn().mockResolvedValue({}),
update: jest.fn().mockResolvedValue({}),
},
$transaction: jest.fn((callback) => callback(mockPrisma)),
};
// Mock AI Services
const mockAIService = {
assignPriorityWithAI: jest.fn().mockResolvedValue('HIGH'),
assessPaymentRisk: jest.fn().mockResolvedValue({ riskLevel: 'LOW' }),
};
// Mock Cache
const mockCache = {
getWorkflow: jest.fn(),
setWorkflow: jest.fn(),
getAIResponse: jest.fn(),
setAIResponse: jest.fn(),
};// Be specific with assertions
expect(result.status).toBe('COMPLETED'); // Not just truthy
// Test error cases
expect(() => service.executeWorkflow('invalid', {}, userId))
.toThrow(WorkflowError);
// Test edge cases
expect(service.getWorkflow('')).toBeNull();
expect(service.getWorkflow(null as any)).toBeNull();| Component | Target Coverage |
|---|---|
| WorkflowEngineService | 85% |
| WorkflowCacheService | 90% |
| WorkflowRateLimiterService | 90% |
| WorkflowMetricsService | 85% |
| WorkflowParallelExecutor | 95% |
| WorkflowSchedulerService | 80% |
| WorkflowAIHelper | 85% |
- ✅ Error handling paths
- ✅ Retry logic
- ✅ Circuit breaker state transitions
- ✅ Rate limiting enforcement
- ✅ Cache expiration
- ✅ Transaction rollback
- ✅ Permission checks
- ✅ Input validation
# Run all tests
npm test
# Run with coverage
npm run test:coverage
# Run specific test file
npm test workflow-cache.service.spec.ts
# Run in watch mode
npm run test:watch
# Run E2E tests
npm run test:e2e- Security:
eval()replacement with safe parser - Database persistence: Executions saved to DB
- Transactions: Rollback on failure
- Retry logic: Exponential backoff works
- Input validation: Invalid inputs rejected
- Permission checks: Unauthorized access blocked
- Scheduler: Race condition prevention
- Structured logging: Correlation IDs present
- Error codes: Proper error codes returned
- Circuit breaker: Opens/closes correctly
- Retry wrapper: AI calls retry on failure
- Metrics: Metrics recorded correctly
- Dead letter queue: Failed executions stored
- Caching: Workflow definitions cached
- Caching: AI responses cached
- Rate limiting: Limits enforced
- Parallel execution: Independent steps run in parallel
- Query optimization: No N+1 queries
- Checkpointing: State saved after steps
| Operation | Target | Measurement |
|---|---|---|
| Workflow lookup (cached) | <1ms | Cache hit |
| Workflow lookup (uncached) | <50ms | Database query |
| AI response (cached) | <1ms | Cache hit |
| AI response (uncached) | <2s | API call + retry |
| Parallel step execution | 30-50% faster | vs sequential |
| Database query (optimized) | <50ms | Single query |
describe('Load Testing', () => {
it('should handle concurrent workflow executions', async () => {
const concurrentExecutions = 100;
const promises = Array(concurrentExecutions).fill(null).map(() =>
service.executeWorkflow('test-workflow', {}, userId)
);
const results = await Promise.all(promises);
expect(results).toHaveLength(concurrentExecutions);
expect(results.every(r => r.status === 'COMPLETED')).toBe(true);
});
it('should enforce rate limits under load', async () => {
const limit = 10;
const promises = Array(limit + 5).fill(null).map(() =>
service.executeWorkflow('test-workflow', {}, userId)
);
const results = await Promise.allSettled(promises);
const rejected = results.filter(r => r.status === 'rejected');
expect(rejected.length).toBeGreaterThan(0);
});
});# .github/workflows/test.yml
name: Workflow Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- run: npm install
- run: npm run test:coverage
- run: npm run test:e2e# Run tests before commit
npm run test
npm run lint-
Flaky Tests
- Use proper timeouts
- Wait for async operations
- Clear mocks between tests
-
Database State
- Use transactions for isolation
- Clean up test data
- Use test database
-
Timing Issues
- Use
fakeTimersfor time-dependent tests - Add appropriate delays
- Use
waitForfor async assertions
- Use
Last Updated: January 2025
Maintainer: Development Team