ZoopFlow Testing Framework
Table of Contents
- Introduction to Testing in ZoopFlow
- Testing Philosophy and Approach
- Types of Tests
- Testing Tools and Setup
- Mocking Temporal Dependencies
- Testing Steps in Isolation
- Testing Flows
- Testing Signals
- Testing Error Handling
- Test Contexts and Utilities
- Test Assertions and Matchers
- Performance Testing
- Test Organization and Best Practices
- Continuous Integration
- Examples of Different Test Types
Introduction to Testing in ZoopFlow
Testing is a critical component of the ZoopFlow framework, ensuring reliable behavior across all components of the workflow system. ZoopFlow's layered architecture requires a comprehensive testing strategy that addresses each layer independently while also ensuring they work together properly.
This guide covers the testing infrastructure, approaches, and best practices for ZoopFlow. It includes details on testing individual steps, complete flows, signal handling, error handling, and integration with Temporal.io.
Testing Philosophy and Approach
ZoopFlow's testing philosophy is built around these key principles:
-
Test-driven development (TDD): Write tests before implementing features to ensure proper test coverage and drive design decisions.
-
Layer-specific testing: Each layer of the architecture should have dedicated tests:
- Step-level tests for individual task units
- Flow-level tests for workflow orchestration
- Integration tests for system boundaries
- End-to-end tests for full execution paths
-
Isolation: Tests should be isolated from each other, with no shared state or dependencies between test cases.
-
Determinism: Tests should produce the same results every time they are run, regardless of environment or timing.
-
Mocking dependencies: External dependencies should be mocked to ensure tests focus on the unit under test.
-
Comprehensive coverage: Tests should cover both happy paths and error paths, ensuring robust error handling.
Types of Tests
Unit Tests
Unit tests focus on testing individual components in isolation. In ZoopFlow, this primarily means testing:
- Individual step implementations
- Flow definitions
- Signal handlers
- Utility functions and helpers
- Error handling mechanisms
Unit tests should be small, fast, and focused on a single piece of functionality.
Integration Tests
Integration tests verify that different parts of the system work correctly together. In ZoopFlow, integration tests typically focus on:
- Flow execution with real step implementations
- Signal handling across workflow boundaries
- Temporal.io integration
- Error propagation between components
End-to-End Tests
End-to-end tests verify the entire system works together, including external dependencies. These tests typically:
- Execute complete flows with real dependencies
- Verify proper execution and state management
- Test error handling and recovery
- Validate Temporal integration in realistic scenarios
Testing Tools and Setup
ZoopFlow uses Jest as its primary testing framework, along with custom utilities for workflow testing.
Jest Configuration
The Jest configuration is defined in jest.config.mjs and includes:
export default {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/__tests__/**/*.test.ts'],
setupFilesAfterEnv: ['<rootDir>/src/__tests__/setup.ts'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/__tests__/**',
'!src/**/__mocks__/**',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
};Test Environment Setup
The setup file (src/__tests__/setup.ts) initializes the test environment:
/**
* Jest setup file
* This file is executed before each test file
*/
// Global test setup
beforeAll(() => {
// Global setup code that runs once before all tests
jest.setTimeout(10000); // Set default timeout to 10 seconds
});
afterAll(() => {
// Global teardown code that runs once after all tests
});
// Reset mocks before each test
beforeEach(() => {
jest.resetAllMocks();
});Mocking Temporal Dependencies
Temporal.io is a critical dependency that requires careful mocking for effective testing.
Workflow Mocks
ZoopFlow uses a dedicated mock for @temporalio/workflow in src/__mocks__/@temporalio/workflow.ts:
/**
* Mock for @temporalio/workflow module
*/
// Mock sleep function
export const sleep = jest.fn().mockImplementation((timeMs: number) => {
return new Promise(resolve => setTimeout(resolve, timeMs));
});
// Mock executeChild function (used for scoped execution)
export const executeChild = jest.fn().mockImplementation((fn: () => any) => {
return fn();
});
// Mock setHandler function
export const setHandler = jest.fn();
// Mock info object
export const info = {
workflowId: 'mock-workflow-id',
runId: 'mock-run-id',
workflowType: 'mock-workflow-type',
taskQueue: 'mock-task-queue',
continueAsNew: false,
attempt: 1,
startTime: new Date(),
firstExecutionRunId: 'mock-first-execution-run-id',
};
// Mock memo object
export const memo = {
retryableErrorTypes: ['RETRYABLE_ERROR'],
errorRecoveryStrategy: 'retry-with-backoff',
};
// Provide a mockWorkflow object for tests
export const mockWorkflow = {
sleep,
executeChild,
setHandler,
info,
memo,
};
export default mockWorkflow;Testing with TestWorkflowEnvironment
For integration tests that need a more complete Temporal environment, use TestWorkflowEnvironment:
import { TestWorkflowEnvironment } from '@temporalio/testing';
import { Worker } from '@temporalio/worker';
describe('Temporal Integration Tests', () => {
let testEnv: TestWorkflowEnvironment;
let worker: Worker;
beforeAll(async () => {
// Set up the test environment
testEnv = await TestWorkflowEnvironment.create();
// Create the worker
worker = await Worker.create({
connection: testEnv.nativeConnection,
taskQueue: 'test-queue',
workflowsPath: require.resolve('../../core/temporal/workflows'),
activities: {
// Mock activities
'test.activity': async (input) => {
return { result: `Processed: ${JSON.stringify(input)}` };
},
},
});
// Start the worker
await worker.start();
});
afterAll(async () => {
// Shut down the worker and test environment
await worker.shutdown();
await testEnv.teardown();
});
it('should execute a workflow', async () => {
// Start a workflow
const handle = await testEnv.client.workflow.start('myWorkflow', {
taskQueue: 'test-queue',
args: [/* workflow arguments */],
});
// Wait for result
const result = await handle.result();
expect(result).toEqual(/* expected result */);
});
});Testing Steps in Isolation
Step Test Harness
The Step Test Harness provides a way to test individual steps in isolation:
// Example usage of the Step Test Harness
import { createStepTestHarness } from '../helpers';
import { reverseStringStep } from '../../core/examples/steps/string-transformer';
describe('reverseStringStep', () => {
const harness = createStepTestHarness(reverseStringStep);
beforeEach(() => {
// Mock services if needed
harness.mockServices({
loggingService: {
logEvent: jest.fn(),
},
});
});
it('should reverse the input string', async () => {
const result = await harness.execute({ text: 'hello' });
expect(result).toEqual({
result: 'olleh',
metadata: expect.objectContaining({
operation: 'reverse',
})
});
});
it('should throw error for invalid input', async () => {
await expect(
harness.execute({ invalidInput: 'test' } as any)
).rejects.toThrow();
});
});Mocking Step Context
For step tests, you can use the TestStepContext class to simulate the step execution context:
/**
* Create a mock step context for testing
*/
export function createMockStepContext() {
return new TestStepContext({
stepId: 'test.step',
flowId: 'test.flow',
executionId: 'test-execution-id',
attemptNumber: 1,
});
}
// In your tests:
const context = createMockStepContext();
jest.spyOn(context, 'log');
jest.spyOn(context, 'setState');
const result = await myStep.execute(input, context);
expect(context.log).toHaveBeenCalledWith(
'Step execution started',
expect.any(Object)
);Testing Step Input/Output
Test step validation by checking input and output schema validation:
it('should validate input correctly', () => {
const step = numberTransformerStep;
// Valid input
expect(step.validateInput({ value: 42 })).toBe(true);
// Invalid inputs
expect(step.validateInput({ value: 'not-a-number' })).toBe(false);
expect(step.validateInput({ invalidProp: 42 })).toBe(false);
});
it('should validate output correctly', () => {
const step = numberTransformerStep;
// Valid output
const validOutput = {
result: 84,
metadata: {
operation: 'multiply',
factor: 2,
},
};
expect(step.validateOutput(validOutput)).toBe(true);
// Invalid output
const invalidOutput = {
result: 'not-a-number',
metadata: {},
};
expect(step.validateOutput(invalidOutput)).toBe(false);
});Testing Flows
Flow Test Harness
The Flow Test Harness allows testing complete flows while mocking individual steps:
import { createFlowTestHarness } from '../helpers';
import { textProcessingFlow } from '../../core/examples/flows/text-processing-flow';
import { upperCaseStep } from '../../core/examples/steps/string-transformer';
import { reverseStringStep } from '../../core/examples/steps/string-transformer';
describe('textProcessingFlow', () => {
const harness = createFlowTestHarness(textProcessingFlow);
beforeEach(() => {
// Mock steps
harness.mockStep(upperCaseStep, async (input) => ({
result: input.text.toUpperCase(),
metadata: { operation: 'uppercase' },
}));
harness.mockStep(reverseStringStep, async (input) => ({
result: input.text.split('').reverse().join(''),
metadata: { operation: 'reverse' },
}));
});
it('should process text correctly', async () => {
const result = await harness.execute({
text: 'hello world',
});
expect(result).toEqual({
result: 'DLROW OLLEH',
metadata: {
appliedOperations: ['uppercase', 'reverse'],
timestamp: expect.any(String),
},
});
});
});Testing Flow Execution
Test the flow execution path and state management:
it('should execute flow steps in the correct order', async () => {
const executionHistory = await harness.getExecutionHistory();
// Verify step execution sequence
expect(executionHistory).toContainEqual(
expect.objectContaining({
type: 'STEP_COMPLETED',
stepId: 'upperCaseStep',
})
);
expect(executionHistory).toContainEqual(
expect.objectContaining({
type: 'STEP_COMPLETED',
stepId: 'reverseStringStep',
})
);
// Verify execution order
const upperCaseIndex = executionHistory.findIndex(
event => event.type === 'STEP_COMPLETED' && event.stepId === 'upperCaseStep'
);
const reverseIndex = executionHistory.findIndex(
event => event.type === 'STEP_COMPLETED' && event.stepId === 'reverseStringStep'
);
expect(upperCaseIndex).toBeLessThan(reverseIndex);
});Testing Flow Validation
Test the flow definition validation logic:
import { defineFlow } from '../../core/flow/define-flow';
it('should validate required flow options', () => {
// Missing ID
expect(() =>
defineFlow({
version: '1.0.0',
inputSchema: { type: 'object' },
outputSchema: { type: 'object' },
steps: async () => ({}),
} as any)
).toThrow('Flow must have an id');
// Invalid version format
expect(() =>
defineFlow({
id: 'test.flow',
version: '1.0', // Invalid format
inputSchema: { type: 'object' },
outputSchema: { type: 'object' },
steps: async () => ({}),
})
).toThrow('Flow version must be in format "x.y.z"');
});Testing Signals
Signal Registration Tests
Test signal registration and validation:
import { defineSignal } from '../../core/flow/define-signal';
import { SignalRegistry } from '../../core/flow/signal-registry';
describe('Signal Registration', () => {
let registry: SignalRegistry;
beforeEach(() => {
registry = new SignalRegistry();
});
it('should register a signal', () => {
const signalDef = defineSignal<{ test: string }>({
id: 'test.signal',
payloadSchema: { type: 'object' },
handler: async () => {},
});
registry.registerSignal(signalDef);
expect(registry.hasSignal('test.signal')).toBe(true);
expect(registry.getSignal('test.signal')).toBe(signalDef);
});
it('should throw an error if registering duplicate signal ID', () => {
const signalDef1 = defineSignal({
id: 'test.duplicate',
payloadSchema: { type: 'object' },
handler: async () => {},
});
const signalDef2 = defineSignal({
id: 'test.duplicate',
payloadSchema: { type: 'object' },
handler: async () => {},
});
registry.registerSignal(signalDef1);
expect(() => registry.registerSignal(signalDef2)).toThrow(/already registered/);
});
});Signal Handler Tests
Test signal handler execution:
import { defineSignal } from '../../core/flow/define-signal';
import { TestFlowContext } from '../helpers/test-context';
it('should execute handler function with payload and context', async () => {
const mockContext = new TestFlowContext();
const handlerFn = jest.fn().mockResolvedValue(undefined);
const signalDef = defineSignal<{ value: string }>({
id: 'test.signal',
payloadSchema: {
type: 'object',
properties: {
value: { type: 'string' },
},
},
handler: handlerFn,
});
const payload = { value: 'test-value' };
await signalDef.handler(payload, mockContext);
expect(handlerFn).toHaveBeenCalledWith(payload, mockContext);
});Signal Integration Tests
Test signals with a complete workflow:
import { WorkflowClient } from '@temporalio/client';
import { TestWorkflowEnvironment } from '@temporalio/testing';
import { Worker } from '@temporalio/worker';
import { TemporalBridge } from '../../core/temporal/temporal-bridge';
import { createFlowControlSignals } from '../../core/temporal/signalHandlers';
describe('Signal Integration Tests', () => {
let testEnv: TestWorkflowEnvironment;
let worker: Worker;
let client: WorkflowClient;
let temporalBridge: TemporalBridge;
let handle;
beforeAll(async () => {
// Setup test environment
testEnv = await TestWorkflowEnvironment.create();
client = testEnv.client;
temporalBridge = new TemporalBridge({
connection: testEnv.nativeConnection,
taskQueue: 'test-queue',
});
// Register signals
const signals = createFlowControlSignals();
signals.forEach(signal => {
temporalBridge.registerSignalForFlow('test.flow', signal);
});
// Create worker
worker = await Worker.create({
connection: testEnv.nativeConnection,
taskQueue: 'test-queue',
workflowsPath: require.resolve('../../core/temporal/workflows'),
activities: {
'test.activity': async (input) => ({ result: input }),
},
});
await worker.start();
});
beforeEach(async () => {
// Start workflow
handle = await client.start('executeFlow', {
taskQueue: 'test-queue',
args: ['test.flow', { input: 'test' }],
});
await new Promise(resolve => setTimeout(resolve, 100));
});
it('should pause a workflow when pause signal is sent', async () => {
// Send pause signal
await handle.signal('flow.pause', { reason: 'Test pause' });
await new Promise(resolve => setTimeout(resolve, 100));
// Get workflow status
const state = await handle.query('getState');
expect(state.status).toBe('PAUSED');
expect(state.pauseReason).toBe('Test pause');
});
afterAll(async () => {
await worker.shutdown();
await testEnv.teardown();
});
});Testing Error Handling
Error Classes
Test custom error classes and their behavior:
import {
FlowError,
FlowValidationError,
FlowExecutionError,
FlowErrorUtils,
} from '../../core/error/flow-errors';
describe('Flow Error Classes', () => {
it('should create a base flow error with default values', () => {
const error = new FlowError('Test error', 'TEST_ERROR');
expect(error.message).toBe('Test error');
expect(error.errorCode).toBe('TEST_ERROR');
expect(error.category).toBe(ErrorCategory.OTHER);
expect(error.severity).toBe(ErrorSeverity.ERROR);
expect(error.recoverable).toBe(false);
});
it('should convert unknown error to FlowError', () => {
const originalError = new Error('Original error');
const flowError = FlowErrorUtils.toFlowError(originalError);
expect(flowError).toBeInstanceOf(FlowExecutionError);
expect(flowError.message).toBe('Original error');
expect(flowError.cause).toBe(originalError);
});
});Error Propagation
Test error propagation through the flow:
import { FlowInterpreter } from '../../core/interpreter/flow-interpreter';
import { NodeHandlerRegistry } from '../../core/interpreter/node-handler-registry';
it('should propagate errors from steps to the flow', async () => {
const errorStep = {
id: 'error-step',
execute: jest.fn().mockRejectedValue(new Error('Step execution failed')),
};
const registry = new StepRegistry();
registry.registerStep(errorStep);
const flowDef = {
// Flow definition with errorStep
};
const interpreter = new FlowInterpreter(flowDef, registry);
await expect(interpreter.execute({ input: 'test' }))
.rejects.toThrow('Step execution failed');
});Error Recovery
Test error recovery mechanisms:
import { FlowErrorHandler } from '../../core/error/error-handler-registry';
it('should recover from retryable errors', async () => {
const mockStep = {
id: 'retryable-step',
execute: jest.fn()
.mockRejectedValueOnce(new FlowDependencyError('Database error', 'db'))
.mockResolvedValueOnce({ success: true }),
};
const errorHandler = new FlowErrorHandler({
retryStrategy: 'immediate',
maxRetries: 1,
});
const result = await errorHandler.handleStepError(
mockStep.execute, [], mockContext, mockStep
);
expect(mockStep.execute).toHaveBeenCalledTimes(2);
expect(result).toEqual({ success: true });
});Test Contexts and Utilities
TestStepContext
The TestStepContext class provides a mock implementation of the step context:
/**
* Simple test context implementation
*/
export class TestStepContext implements StepContextType {
public logCalls: Array<{ message: string; metadata?: Record<string, unknown> }> = [];
public stateStore: Map<string, any> = new Map();
public serviceStore: Map<string, any> = new Map();
public info: ExecutionInfo;
public stepMetadata: StepMetadata;
constructor(executionInfo?: Partial<ExecutionInfo>, metadata?: StepMetadata) {
this.info = {
stepId: executionInfo?.stepId || 'test.step',
flowId: executionInfo?.flowId || 'test.flow',
executionId: executionInfo?.executionId || 'test-execution-id',
attemptNumber: executionInfo?.attemptNumber || 1,
};
this.stepMetadata = metadata || {
id: 'test.step',
version: '1.0.0',
displayName: 'Test Step',
description: 'A step for testing',
inputSchema: { type: 'object' },
outputSchema: { type: 'object' },
};
}
log(message: string, metadata?: Record<string, unknown>): void {
this.logCalls.push({ message, metadata });
}
async getState<T>(key: string): Promise<T | undefined> {
return this.stateStore.get(key) as T | undefined;
}
async setState<T>(key: string, value: T): Promise<void> {
this.stateStore.set(key, value);
}
getService<T>(name: string, _options?: Record<string, unknown>): T {
return this.serviceStore.get(name) as T;
}
/**
* Register a service for testing
*/
registerService<T>(name: string, service: T): void {
this.serviceStore.set(name, service);
}
}Test Utilities
Test utilities help with common testing tasks:
/**
* Test utilities for common test operations
*/
/**
* Validates that a version string follows semantic versioning (x.y.z)
* @param version The version string to validate
* @returns True if the version is valid, false otherwise
*/
export function isValidVersion(version: string): boolean {
const semverRegex = /^(\d+)\.(\d+)\.(\d+)$/;
return semverRegex.test(version);
}
/**
* Validates that an ID follows the namespace.name format
* @param id The ID to validate
* @returns True if the ID is valid, false otherwise
*/
export function isValidId(id: string): boolean {
const idRegex = /^[a-z0-9-]+\.[a-z0-9-]+$/i;
return idRegex.test(id);
}
/**
* Wait for a specified amount of time
* @param ms Milliseconds to wait
* @returns A promise that resolves after the specified time
*/
export function wait(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}Test Assertions and Matchers
Custom Assertions
ZoopFlow provides custom assertions for common testing scenarios:
/**
* Assert that a value matches a JSON Schema
* @param value The value to validate
* @param schema The JSON Schema to validate against
*/
export function assertMatchesSchema(value: any, schema: Record<string, any>): void {
if (schema.type === 'object') {
expect(typeof value).toBe('object');
expect(value).not.toBeNull();
// Check required properties
if (schema.required) {
schema.required.forEach((prop: string) => {
expect(value).toHaveProperty(prop);
});
}
// Check property types
if (schema.properties) {
Object.entries(schema.properties).forEach(([prop, propSchema]: [string, any]) => {
if (value[prop] !== undefined) {
if (propSchema.type === 'string') {
expect(typeof value[prop]).toBe('string');
} else if (propSchema.type === 'number') {
expect(typeof value[prop]).toBe('number');
} else if (propSchema.type === 'boolean') {
expect(typeof value[prop]).toBe('boolean');
} else if (propSchema.type === 'object') {
expect(typeof value[prop]).toBe('object');
expect(value[prop]).not.toBeNull();
} else if (propSchema.type === 'array') {
expect(Array.isArray(value[prop])).toBe(true);
}
}
});
}
}
// Additional type checks...
}
/**
* Assert that a function throws an error matching a pattern
* @param fn The function to execute
* @param errorPattern The error message pattern to match
*/
export async function assertThrowsAsync(
fn: () => Promise<any>,
errorPattern?: RegExp | string,
): Promise<void> {
let error: Error | null = null;
try {
await fn();
} catch (e) {
error = e as Error;
}
expect(error).not.toBeNull();
if (errorPattern) {
if (errorPattern instanceof RegExp) {
expect(error!.message).toMatch(errorPattern);
} else {
expect(error!.message).toContain(errorPattern);
}
}
}Schema Validation
Test schema validation for steps and flows:
import { SchemaValidator } from '../../core/utils/schema-validator';
describe('Schema Validation', () => {
const validator = new SchemaValidator();
const schema = {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' },
},
required: ['name'],
};
it('should validate valid data', () => {
const result = validator.validate({ name: 'John', age: 30 }, schema);
expect(result.valid).toBe(true);
expect(result.errors).toBeUndefined();
});
it('should reject invalid data', () => {
const result = validator.validate({ age: 'thirty' }, schema);
expect(result.valid).toBe(false);
expect(result.errors).toBeDefined();
if (result.errors) {
expect(result.errors).toContainEqual(
expect.objectContaining({
path: expect.stringContaining('name'),
message: expect.stringContaining('required'),
})
);
expect(result.errors).toContainEqual(
expect.objectContaining({
path: expect.stringContaining('age'),
message: expect.stringContaining('number'),
})
);
}
});
});Performance Testing
Measuring Execution Time
Measure step and flow execution times:
it('should execute within performance threshold', async () => {
const startTime = Date.now();
await myStep.execute(input, context);
const duration = Date.now() - startTime;
expect(duration).toBeLessThan(100); // 100ms threshold
});Load Testing
Test performance under load:
it('should handle concurrent executions', async () => {
const concurrentExecutions = 10;
const startTime = Date.now();
// Execute multiple flows concurrently
const promises = Array(concurrentExecutions).fill(0).map((_, i) =>
flow.execute({ id: `test-${i}` })
);
const results = await Promise.all(promises);
const duration = Date.now() - startTime;
const avgDuration = duration / concurrentExecutions;
expect(results).toHaveLength(concurrentExecutions);
expect(avgDuration).toBeLessThan(500); // 500ms average threshold
});Test Organization and Best Practices
File Structure
Follow this file structure for tests:
src/
├── __tests__/
│ ├── core/
│ │ ├── flow/
│ │ │ ├── define-flow.test.ts
│ │ │ ├── signal-handling.test.ts
│ │ ├── steps/
│ │ │ ├── string-transformer.test.ts
│ │ │ ├── number-transformer.test.ts
│ │ ├── error/
│ │ │ ├── flow-errors.test.ts
│ ├── integration/
│ │ ├── flowTemporal.test.ts
│ ├── helpers/
│ │ ├── assertions.ts
│ │ ├── test-context.ts
│ │ ├── test-utils.ts
│ │ ├── index.ts
│ ├── setup.tsNaming Conventions
Follow these naming conventions:
- Test files should be named
*.test.tsto match Jest's test patterns - Use descriptive test names that indicate what's being tested
- Use lowercase for directory names and kebab-case for test files
- Use descriptive
describeanditblocks that form readable sentences
Example:
describe('StringTransformer', () => {
describe('reverseString', () => {
it('should reverse the input string', () => {
// Test code
});
it('should return empty string when input is empty', () => {
// Test code
});
});
});Test Coverage
Maintain high test coverage:
- Aim for at least 80% code coverage across branches, functions, and lines
- Focus on testing complex logic and error paths
- Run coverage reports regularly with
npm run test:coverage - Add tests for all new features and bug fixes
Continuous Integration
CI Pipeline Configuration
Configure CI for automated testing:
# Example CI configuration
name: Test
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '16.x'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Test
run: npm run test
- name: Coverage
run: npm run test:coverage
- name: Upload coverage reports
uses: codecov/codecov-action@v1Automated Testing
Implement automated testing best practices:
- Run tests on every PR and push to main branches
- Require passing tests before merging
- Generate and publish test coverage reports
- Monitor test execution time and performance
- Automatically flag flaky tests for investigation
Examples of Different Test Types
Step Test Example
// Example step test
import { createStepTestHarness } from '../helpers';
import { upperCaseStep } from '../../core/examples/steps/string-transformer';
describe('upperCaseStep', () => {
const harness = createStepTestHarness(upperCaseStep);
it('should convert string to uppercase', async () => {
const result = await harness.execute({ text: 'hello world' });
expect(result).toEqual({
result: 'HELLO WORLD',
metadata: {
operation: 'uppercase',
timestamp: expect.any(String),
},
});
});
it('should throw an error if input is missing text property', async () => {
await expect(
harness.execute({} as any)
).rejects.toThrow('required property');
});
it('should throw an error if text is not a string', async () => {
await expect(
harness.execute({ text: 123 } as any)
).rejects.toThrow('must be string');
});
});Flow Test Example
// Example flow test
import { createFlowTestHarness } from '../helpers';
import { textProcessingFlow } from '../../core/examples/flows/text-processing-flow';
describe('textProcessingFlow', () => {
const harness = createFlowTestHarness(textProcessingFlow);
it('should process text correctly with default options', async () => {
const input = { text: 'hello world' };
const result = await harness.execute(input);
// Verify the result
expect(result).toEqual({
result: 'DLROW OLLEH',
metadata: {
appliedOperations: ['uppercase', 'reverse'],
timestamp: expect.any(String),
},
});
// Verify step execution
const history = await harness.getExecutionHistory();
expect(history).toContainEqual(
expect.objectContaining({
type: 'STEP_COMPLETED',
stepId: 'string-transformer.uppercase',
})
);
expect(history).toContainEqual(
expect.objectContaining({
type: 'STEP_COMPLETED',
stepId: 'string-transformer.reverse',
})
);
});
it('should include original text when requested', async () => {
const input = {
text: 'hello world',
options: { includeOriginal: true },
};
const result = await harness.execute(input);
// Verify the result includes original text
expect(result.metadata.originalText).toBe('hello world');
});
});Signal Test Example
// Example signal test
import { defineSignal } from '../../core/flow/define-signal';
import { TestFlowContext } from '../helpers/test-context';
import { FlowExecutionStatus } from '../../core/interfaces/types';
describe('Flow Pause Signal', () => {
it('should update flow status to PAUSED', async () => {
// Create test context
const context = new TestFlowContext();
// Define pause signal
const pauseSignal = defineSignal<{ reason?: string }>({
id: 'flow.pause',
description: 'Signal to pause workflow execution',
payloadSchema: {
type: 'object',
properties: {
reason: { type: 'string' },
},
},
handler: async (payload, ctx) => {
await ctx.setState('status', FlowExecutionStatus.PAUSED);
if (payload.reason) {
await ctx.setState('pauseReason', payload.reason);
}
await ctx.setState('pausedAt', new Date().toISOString());
},
});
// Execute signal handler
await pauseSignal.handler({ reason: 'Manual pause' }, context);
// Verify state changes
const status = await context.getState('status');
const reason = await context.getState('pauseReason');
const pausedAt = await context.getState('pausedAt');
expect(status).toBe(FlowExecutionStatus.PAUSED);
expect(reason).toBe('Manual pause');
expect(pausedAt).toBeDefined();
});
});Integration Test Example
// Example integration test with Temporal
import { TestWorkflowEnvironment } from '@temporalio/testing';
import { Worker } from '@temporalio/worker';
import { TemporalBridge } from '../../core/temporal/temporal-bridge';
import { textProcessingFlow } from '../../core/examples/flows/text-processing-flow';
describe('Flow Temporal Integration', () => {
let testEnv;
let worker;
beforeAll(async () => {
// Setup
testEnv = await TestWorkflowEnvironment.create();
// Create Temporal bridge
const temporalBridge = new TemporalBridge({
connection: testEnv.nativeConnection,
taskQueue: 'test-queue',
});
// Register flow
temporalBridge.registerFlow(textProcessingFlow);
// Create worker
worker = await Worker.create({
connection: testEnv.nativeConnection,
taskQueue: 'test-queue',
workflowsPath: require.resolve('../../core/temporal/flow-workflow'),
activities: {
'string-transformer.uppercase': async (input: { text: string }) => {
return {
result: input.text.toUpperCase(),
metadata: { operation: 'uppercase' },
};
},
'string-transformer.reverse': async (input: { text: string }) => {
return {
result: input.text.split('').reverse().join(''),
metadata: { operation: 'reverse' },
};
},
},
});
await worker.start();
});
afterAll(async () => {
await worker.shutdown();
await testEnv.teardown();
});
it('should execute flow through Temporal', async () => {
// Start workflow
const handle = await testEnv.client.workflow.start('executeFlow', {
taskQueue: 'test-queue',
args: ['zoop.text-processing', { text: 'hello world' }],
});
// Wait for result
const result = await handle.result();
// Verify result
expect(result).toEqual({
result: 'DLROW OLLEH',
metadata: {
appliedOperations: ['uppercase', 'reverse'],
timestamp: expect.any(String),
},
});
});
});