DEV Community

Cover image for Building Confidence at Scale: A Deep Dive into Cipher Horizon's Testing Architecture
Daniele Minatto
Daniele Minatto

Posted on

Building Confidence at Scale: A Deep Dive into Cipher Horizon's Testing Architecture

Testing a microservices architecture presents unique challenges due to distributed components, complex interactions, and various failure modes. This comprehensive guide details how Cipher Horizon implements testing strategies to ensure system reliability and quality.

Understanding Testing Requirements

Before implementing our testing strategy, we identified key requirements:

  1. Coverage Requirements
    • Unit test coverage > 80%
    • Integration test coverage > 70%
    • Critical path E2E coverage 100%
  2. Performance Criteria
    • Response time < 200ms (95th percentile)
    • Throughput > 1000 RPS
    • Error rate < 0.1%
  3. Quality Gates
    • Code quality metrics
    • Security scanning
    • Performance benchmarks

Unit Testing Implementation

Core Testing Framework

Our unit testing strategy focuses on isolation and comprehensive coverage:

// Service Test Implementation with Detailed Mocking
describe('UserService', () => {
    let service: UserService;
    let repository: MockType<Repository<User>>;
    let eventEmitter: MockType<EventEmitter>;
    let cacheManager: MockType<CacheManager>;

    beforeEach(async () => {
        // Create testing module with comprehensive mocking
        const module = await Test.createTestingModule({
            providers: [
                UserService,
                {
                    provide: getRepositoryToken(User),
                    useFactory: repositoryMockFactory
                },
                {
                    provide: EventEmitter,
                    useFactory: eventEmitterMockFactory
                },
                {
                    provide: CacheManager,
                    useFactory: cacheManagerMockFactory
                }
            ]
        }).compile();

        // Get service and mocked dependencies
        service = module.get<UserService>(UserService);
        repository = module.get(getRepositoryToken(User));
        eventEmitter = module.get(EventEmitter);
        cacheManager = module.get(CacheManager);
    });

    describe('createUser', () => {
        it('should create user and emit event', async () => {
            // Arrange
            const userData = {
                email: 'test@example.com',
                name: 'Test User'
            };
            const savedUser = { ...userData, id: '1' };
            repository.save.mockResolvedValue(savedUser);

            // Act
            const result = await service.createUser(userData);

            // Assert
            expect(result).toEqual(savedUser);
            expect(repository.save).toHaveBeenCalledWith(userData);
            expect(eventEmitter.emit).toHaveBeenCalledWith(
                'user.created',
                savedUser
            );
            expect(cacheManager.set).toHaveBeenCalledWith(
                `user:${savedUser.id}`,
                savedUser
            );
        });

        // Test error scenarios
        it('should handle database errors gracefully', async () => {
            // Arrange
            repository.save.mockRejectedValue(new DatabaseError());

            // Act & Assert
            await expect(
                service.createUser({ email: 'test@example.com' })
            ).rejects.toThrow(UserCreationError);
            expect(eventEmitter.emit).not.toHaveBeenCalled();
            expect(cacheManager.set).not.toHaveBeenCalled();
        });
    });
});
Enter fullscreen mode Exit fullscreen mode

Testing Utilities

We developed robust testing utilities to support our testing strategy:

// Advanced Mock Factory
class MockFactory<T> {
    create(partial: Partial<T> = {}): jest.Mocked<T> {
        const mock = {
            ...this.getDefaultMock(),
            ...partial
        };

        return this.addSpies(mock);
    }

    private getDefaultMock(): Partial<T> {
        // Implementation specific to each type
        return {};
    }

    private addSpies(mock: T): jest.Mocked<T> {
        const spiedMock = { ...mock };

        Object.keys(mock).forEach(key => {
            if (typeof mock[key] === 'function') {
                spiedMock[key] = jest.fn();
            }
        });

        return spiedMock as jest.Mocked<T>;
    }
}

// Test Data Factory
class TestDataFactory {
    static createUser(override: Partial<User> = {}): User {
        return {
            id: uuid(),
            email: `test-${Date.now()}@example.com`,
            name: 'Test User',
            createdAt: new Date(),
            ...override
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

Integration Testing Framework

Our integration testing strategy ensures proper service interaction:

@Injectable()
class IntegrationTestManager {
    constructor(
        private readonly dbConnection: Connection,
        private readonly redisClient: Redis,
        private readonly kafkaClient: KafkaClient,
        private readonly metrics: TestMetrics
    ) {}

    async setupTestEnvironment(
        config: TestEnvironmentConfig
    ): Promise<TestEnvironment> {
        // Create isolated test environment
        const environment = await this.createIsolatedEnvironment(config);

        try {
            // Initialize test dependencies
            await this.initializeDependencies(environment);

            // Seed test data
            await this.seedTestData(environment);

            // Setup monitoring
            await this.setupMonitoring(environment);

            return environment;
        } catch (error) {
            // Cleanup on failure
            await this.teardown(environment);
            throw error;
        }
    }

    async runIntegrationTest(
        test: (env: TestEnvironment) => Promise<void>
    ): Promise<TestResult> {
        const startTime = Date.now();
        const environment = await this.setupTestEnvironment({
            isolationLevel: 'high',
            cleanup: true
        });

        try {
            await test(environment);
            return this.createSuccessResult(Date.now() - startTime);
        } catch (error) {
            return this.createFailureResult(error, Date.now() - startTime);
        } finally {
            await this.teardown(environment);
        }
    }
}

// Integration Test Example
describe('Order Processing Flow', () => {
    let testManager: IntegrationTestManager;
    let testEnvironment: TestEnvironment;

    beforeAll(async () => {
        testManager = await TestContainer.get(IntegrationTestManager);
    });

    beforeEach(async () => {
        testEnvironment = await testManager.setupTestEnvironment({
            services: ['order', 'payment', 'inventory'],
            databases: ['postgres', 'redis'],
            messageQueues: ['kafka']
        });
    });

    it('should process order end-to-end', async () => {
        await testManager.runIntegrationTest(async (env) => {
            // Create order
            const order = await env.orderService.createOrder({
                items: [{ productId: '1', quantity: 2 }],
                userId: 'test-user'
            });

            // Verify inventory check
            const inventoryCheck = await env.inventoryService
                .getInventoryCheck(order.id);
            expect(inventoryCheck.status).toBe('RESERVED');

            // Process payment
            const payment = await env.paymentService.processPayment({
                orderId: order.id,
                amount: order.totalAmount
            });
            expect(payment.status).toBe('COMPLETED');

            // Verify final order status
            const updatedOrder = await env.orderService
                .getOrder(order.id);
            expect(updatedOrder.status).toBe('PAID');
            expect(updatedOrder.paymentId).toBe(payment.id);

            // Verify events
            const events = await env.eventStore.getEvents(order.id);
            expect(events).toMatchSnapshot();
        });
    });
});
Enter fullscreen mode Exit fullscreen mode

E2E Testing Implementation

Our end-to-end testing strategy focuses on real-world scenarios and user journeys:

E2E Test Framework

@Injectable()
class E2ETestSuite {
    constructor(
        private readonly config: E2EConfig,
        private readonly metrics: TestMetrics,
        private readonly logger: TestLogger
    ) {}

    async runE2EScenario(
        scenario: E2EScenario
    ): Promise<E2ETestResult> {
        const context = await this.createTestContext(scenario);
        const monitor = new PerformanceMonitor(context);

        try {
            // Start monitoring
            monitor.start();

            // Execute scenario steps
            for (const step of scenario.steps) {
                await this.executeStep(step, context);
                await this.validateStepResults(step, context);
            }

            // Collect results
            const results = await monitor.collectResults();
            return this.createSuccessResult(results);
        } catch (error) {
            return this.handleTestFailure(error, monitor);
        } finally {
            await this.cleanup(context);
        }
    }

    private async executeStep(
        step: E2EStep,
        context: TestContext
    ): Promise<void> {
        this.logger.info(`Executing step: ${step.name}`);

        const startTime = Date.now();
        try {
            await step.execute(context);

            this.metrics.recordStepExecution({
                step: step.name,
                duration: Date.now() - startTime,
                status: 'success'
            });
        } catch (error) {
            this.metrics.recordStepExecution({
                step: step.name,
                duration: Date.now() - startTime,
                status: 'failure',
                error
            });
            throw error;
        }
    }
}

// E2E Test Scenario Example
describe('User Registration and Order Flow', () => {
    let e2eSuite: E2ETestSuite;

    beforeAll(async () => {
        e2eSuite = new E2ETestSuite({
            baseUrl: process.env.API_URL,
            timeout: 30000,
            retries: 2
        });
    });

    it('should complete full user journey', async () => {
        const result = await e2eSuite.runE2EScenario({
            name: 'New User Purchase Flow',
            steps: [
                {
                    name: 'Register User',
                    execute: async (context) => {
                        const response = await axios.post(
                            '/api/users',
                            {
                                email: 'test@example.com',
                                password: 'password123'
                            }
                        );
                        context.userId = response.data.id;
                    },
                    validate: async (context) => {
                        const user = await getUserById(context.userId);
                        expect(user.status).toBe('ACTIVE');
                    }
                },
                {
                    name: 'Add Product to Cart',
                    execute: async (context) => {
                        await axios.post(
                            '/api/cart',
                            {
                                userId: context.userId,
                                productId: 'test-product',
                                quantity: 1
                            }
                        );
                    }
                },
                {
                    name: 'Complete Purchase',
                    execute: async (context) => {
                        const response = await axios.post(
                            '/api/orders',
                            {
                                userId: context.userId,
                                paymentMethod: 'credit_card',
                                billingDetails: {
                                    // billing details
                                }
                            }
                        );
                        context.orderId = response.data.id;
                    },
                    validate: async (context) => {
                        const order = await getOrderById(context.orderId);
                        expect(order.status).toBe('COMPLETED');
                    }
                }
            ]
        });

        expect(result.success).toBeTruthy();
        expect(result.metrics.totalDuration).toBeLessThan(5000);
    });
});
Enter fullscreen mode Exit fullscreen mode

Performance Testing Implementation

Load Testing Framework

@Injectable()
class PerformanceTestRunner {
    constructor(
        private readonly metrics: MetricsCollector,
        private readonly config: TestConfig,
        private readonly monitor: SystemMonitor
    ) {}

    async runLoadTest(
        scenario: LoadTestScenario
    ): Promise<LoadTestResults> {
        const results = new LoadTestResults();
        const systemMetrics = await this.monitor.startMonitoring();

        try {
            await this.executeWithConcurrency(
                scenario.concurrentUsers,
                async (userId) => {
                    const userMetrics = await this.executeUserScenario(
                        scenario,
                        userId
                    );
                    results.addUserMetrics(userMetrics);
                }
            );

            const systemResults = await systemMetrics.collect();
            return this.analyzeResults(results, systemResults);
        } finally {
            await systemMetrics.stop();
        }
    }

    private async executeUserScenario(
        scenario: LoadTestScenario,
        userId: string
    ): Promise<UserMetrics> {
        const metrics = new UserMetrics(userId);

        for (const action of scenario.actions) {
            const startTime = Date.now();

            try {
                await action.execute();
                metrics.recordSuccess(
                    action.name,
                    Date.now() - startTime
                );
            } catch (error) {
                metrics.recordError(
                    action.name,
                    error,
                    Date.now() - startTime
                );
            }
        }

        return metrics;
    }
}

// Load Test Scenario Example
describe('API Performance', () => {
    let performanceRunner: PerformanceTestRunner;

    beforeAll(async () => {
        performanceRunner = new PerformanceTestRunner({
            duration: '5m',
            rampUp: '30s',
            coolDown: '30s'
        });
    });

    it('should handle high concurrent users', async () => {
        const results = await performanceRunner.runLoadTest({
            name: 'High Concurrency Test',
            concurrentUsers: 1000,
            actions: [
                {
                    name: 'Get Products',
                    weight: 0.7,
                    execute: async () => {
                        return axios.get('/api/products');
                    }
                },
                {
                    name: 'Create Order',
                    weight: 0.3,
                    execute: async () => {
                        return axios.post('/api/orders', {
                            // order details
                        });
                    }
                }
            ],
            assertions: [
                {
                    metric: 'responseTime',
                    percentile: 95,
                    threshold: 200 // ms
                },
                {
                    metric: 'errorRate',
                    max: 0.01 // 1%
                }
            ]
        });

        expect(results.assertions).toAllPass();
    });
});

Enter fullscreen mode Exit fullscreen mode

Best Practices and Lessons Learned

Test Organization

interface TestingBestPractices {
    organization: {
        unitTests: {
            location: 'alongside source files';
            naming: '*.spec.ts';
            grouping: 'by feature';
        };
        integrationTests: {
            location: 'tests/integration';
            naming: '*.integration.spec.ts';
            isolation: 'per test suite';
        };
        e2eTests: {
            location: 'tests/e2e';
            naming: '*.e2e.spec.ts';
            environment: 'isolated';
        };
    };
    practices: {
        mocking: 'minimal, strategic';
        dataManagement: 'isolated, repeatable';
        assertions: 'meaningful, specific';
    };
}
Enter fullscreen mode Exit fullscreen mode

Test Data Management

@Injectable()
class TestDataManager {
    async setupTestData(
        config: TestDataConfig
    ): Promise<TestData> {
        const transaction = await this.db.startTransaction();

        try {
            const data = await this.generateTestData(config);
            await this.validateTestData(data);
            await transaction.commit();
            return data;
        } catch (error) {
            await transaction.rollback();
            throw error;
        }
    }

    private async generateTestData(
        config: TestDataConfig
    ): Promise<TestData> {
        // Implementation for generating test data
        // based on configuration
    }
}
Enter fullscreen mode Exit fullscreen mode

Continuous Testing Pipeline

# CI/CD Pipeline for Testing
name: Continuous Testing

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: '18'

      - name: Install Dependencies
        run: npm ci

      - name: Run Unit Tests
        run: npm run test:unit

      - name: Run Integration Tests
        run: npm run test:integration

      - name: Run E2E Tests
        run: npm run test:e2e

      - name: Upload Test Results
        uses: actions/upload-artifact@v2
        with:
          name: test-results
          path: coverage/
Enter fullscreen mode Exit fullscreen mode

Looking Ahead: Reflecting on the Journey

As we conclude our testing implementation, our next post will focus on reflecting on the entire Cipher Horizon journey, including:

  1. Architecture Evolution
    • Initial design decisions
    • Architecture adaptations
    • Scaling challenges
    • Future improvements
  2. Team Learning
    • Development practices
    • Collaboration strategies
    • Knowledge sharing
    • Skills development
  3. Technical Insights
    • Technology choices
    • Performance optimizations
    • Security considerations
    • Maintenance strategies

What testing strategies have proven most effective in your microservices architecture? Share your experiences in the comments below!

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, valued within the supportive DEV Community. Coders of every background are welcome to join in and add to our collective wisdom.

A sincere "thank you" often brightens someone’s day. Share your gratitude in the comments below!

On DEV, the act of sharing knowledge eases our journey and fortifies our community ties. Found value in this? A quick thank you to the author can make a significant impact.

Okay