CROP
ProjectsParts Services

CROP Microservices - Comprehensive Testing Guide

1. Overview(overview) 2. Testing Strategy(testing-strategy) 3. Test Types(test-types) 4. Running Tests(running-tests) 5. Unit Testing(unit-testing) 6....

CROP Microservices - Comprehensive Testing Guide

Table of Contents

  1. Overview
  2. Testing Strategy
  3. Test Types
  4. Running Tests
  5. Unit Testing
  6. Integration Testing
  7. E2E Testing
  8. Testing Best Practices
  9. CI/CD Integration
  10. Troubleshooting

Overview

This document provides comprehensive guidance on testing the CROP microservices monorepo. Our testing strategy emphasizes:

  • Fast feedback through unit tests
  • Confidence through integration tests
  • Reliability through E2E tests
  • Automation through CI/CD

Test Coverage Goals

  • Unit Tests: 80%+ coverage for shared packages
  • Integration Tests: All API endpoints
  • E2E Tests: Critical user flows

Testing Tools

  • Test Runner: Bun test (built-in, fast)
  • Assertions: expect() from Bun
  • Mocking: @crop/shared-testing utilities
  • Type Safety: TypeScript + Zod schemas

Testing Strategy

Test Pyramid

        /\
       /  \     E2E Tests (Few, Slow, High Confidence)
      /----\
     /      \   Integration Tests (Some, Medium Speed)
    /--------\
   /          \ Unit Tests (Many, Fast, Focused)
  /__________\

What to Test

DO Test:

  • Business logic and algorithms
  • API contract compliance (schemas)
  • Error handling and edge cases
  • Integration points between services
  • Critical user workflows

DON'T Test:

  • Third-party library internals
  • Framework code (Hono, MongoDB, etc.)
  • Simple getters/setters
  • Configuration files

Test Types

1. Unit Tests

Purpose: Test isolated units of code (functions, classes)

Characteristics:

  • Fast execution (< 1ms per test)
  • No external dependencies
  • Use mocks/stubs for dependencies
  • Focus on single responsibility

Location: src/__tests__/**/*.test.ts

Example: ```typescript describe('normalizePartNumber', () => { test('should remove special characters', () => { expect(normalizePartNumber('ABC-123')).toBe('abc123'); }); }); ```

2. Integration Tests

Purpose: Test interactions between components/services

Characteristics:

  • Medium execution speed (10-100ms per test)
  • May use test databases or mocked services
  • Test API endpoints end-to-end
  • Validate request/response contracts

Location: src/__tests__/integration/**/*.test.ts

Example: ```typescript describe('GET /api/parts', () => { test('should return paginated parts', async () => { const response = await tester.get('/api/parts?page=1&limit=20'); expect(response.status).toBe(200); expect(response.json.success).toBe(true); expect(response.json.data.items).toBeArray(); }); }); ```

3. E2E Tests

Purpose: Test complete user workflows

Characteristics:

  • Slow execution (seconds per test)
  • Use real services (in Docker)
  • Test critical paths only
  • Validate business outcomes

Location: tests/e2e/**/*.test.ts

Example: ```typescript describe('Part Search Flow', () => { test('user can search and view part details', async () => { // 1. Search for part const searchResponse = await searchPart('ABC123'); expect(searchResponse.results).not.toBeEmpty();

// 2. Get part details
const partId = searchResponse.results[0].id;
const detailsResponse = await getPartDetails(partId);
expect(detailsResponse.partNumber).toBe('ABC123');

// 3. Check fitment
const fitmentResponse = await getPartFitment(partId);
expect(fitmentResponse.equipment).toBeArray();

}); }); ```


Running Tests

All Tests (Monorepo)

```bash

Run all tests across all packages and services

bun test

Run with watch mode

bun test --watch

Run with coverage

bun test --coverage

Run specific file

bun test src/tests/example.test.ts ```

Per-Package Tests

```bash

Run tests for specific package

cd packages/shared-types bun test

Run tests for specific service

cd services/catalog bun test ```

Docker Tests

```bash

Start infrastructure

make dev

Run integration tests (requires running services)

bun test --grep "integration"

Clean up

make down ```

Test Scripts

```bash

Run all tests with summary

bun run test:all

Run tests in CI mode

bun test --ci

Run tests with specific reporter

bun test --reporter=junit ```


Unit Testing

Using AppTester

The @crop/shared-testing package provides AppTester for testing Hono applications without starting a server:

```typescript import { AppTester } from '@crop/shared-testing'; import { createApp } from '../app';

describe('Catalog API', () => { const tester = new AppTester(createApp());

test('GET /health returns ok', async () => { const response = await tester.get('/health'); expect(response.status).toBe(200); expect(response.json).toEqual({ status: 'ok', service: 'catalog' }); });

test('POST /api/parts creates part', async () => { const response = await tester.post('/api/parts', { body: { partNumber: 'ABC123', manufacturer: 'ACME', }, }); expect(response.status).toBe(201); expect(response.json.success).toBe(true); }); }); ```

Using MockServiceClient

Mock inter-service calls without real HTTP requests:

```typescript import { MockServiceClient } from '@crop/shared-testing';

describe('Search Integration', () => { test('should call search service', async () => { const mockClient = new MockServiceClient('http://localhost:3001');

// Setup mock response
mockClient.mockGet('/api/search', {
  success: true,
  data: {
    results: [{ id: '1', partNumber: 'ABC123' }],
  },
});

// Use mock in your code
const results = await mockClient.get('/api/search', {
  query: { q: 'ABC123' },
});

// Verify
expect(mockClient.wasCalled('GET', '/api/search')).toBe(true);
expect(results.success).toBe(true);

}); }); ```

Using Fixtures

Generate test data consistently:

```typescript import { createMockPart, createMockParts } from '@crop/shared-testing';

describe('Part Processing', () => { test('should process part', () => { const part = createMockPart({ partNumber: 'TEST123', manufacturer: 'TEST', });

const result = processpart(part);
expect(result).toBeDefined();

});

test('should process multiple parts', () => { const parts = createMockParts(5); expect(parts).toHaveLength(5); expect(parts[0].id).toBe('mock-part-1'); }); }); ```

Testing Zod Schemas

```typescript import { PartSchema } from '@crop/shared-types';

describe('PartSchema', () => { test('should validate valid part', () => { const validPart = { id: 'part-1', partNumber: 'ABC123', normalizedPartNumber: 'abc123', manufacturer: 'ACME', createdAt: '2024-01-01T00:00:00.000Z', updatedAt: '2024-01-01T00:00:00.000Z', };

const result = PartSchema.safeParse(validPart);
expect(result.success).toBe(true);

});

test('should reject invalid part', () => { const invalidPart = { id: 'part-1', // missing required fields };

const result = PartSchema.safeParse(invalidPart);
expect(result.success).toBe(false);
if (!result.success) {
  expect(result.error.issues).toBeArray();
}

}); }); ```


Integration Testing

Testing API Endpoints

```typescript import { AppTester } from '@crop/shared-testing'; import { createApp } from '../app';

describe('Catalog API Integration', () => { const tester = new AppTester(createApp());

describe('GET /api/parts', () => { test('should return paginated results', async () => { const response = await tester.get('/api/parts?page=1&limit=10');

  expect(response.status).toBe(200);
  expect(response.json).toMatchObject({
    success: true,
    data: {
      items: expect.any(Array),
      pagination: {
        page: 1,
        limit: 10,
        total: expect.any(Number),
      },
    },
  });
});

test('should filter by manufacturer', async () => {
  const response = await tester.get('/api/parts?manufacturer=ACME');

  expect(response.status).toBe(200);
  expect(response.json.data.items).toSatisfy(items =>
    items.every(item => item.manufacturer === 'ACME')
  );
});

test('should return 400 for invalid pagination', async () => {
  const response = await tester.get('/api/parts?page=-1');

  expect(response.status).toBe(400);
  expect(response.json.success).toBe(false);
  expect(response.json.error.code).toBe('BAD_REQUEST');
});

});

describe('POST /api/parts', () => { test('should create new part', async () => { const response = await tester.post('/api/parts', { body: { partNumber: 'NEW123', manufacturer: 'ACME', description: 'Test part', }, });

  expect(response.status).toBe(201);
  expect(response.json).toMatchObject({
    success: true,
    data: {
      id: expect.any(String),
      partNumber: 'NEW123',
      manufacturer: 'ACME',
    },
  });
});

test('should reject invalid data', async () => {
  const response = await tester.post('/api/parts', {
    body: {
      // missing required fields
      description: 'Test',
    },
  });

  expect(response.status).toBe(400);
  expect(response.json.success).toBe(false);
});

}); }); ```

Testing Inter-Service Communication

```typescript import { MockServiceClient } from '@crop/shared-testing'; import { serviceClients } from '@crop/shared-hono';

describe('Catalog-Search Integration', () => { test('should search parts via search service', async () => { // Setup mock const mockSearch = new MockServiceClient('http://localhost:3001'); mockSearch.mockGet('/api/search', { success: true, data: { results: [ { id: 'part-1', partNumber: 'ABC123', score: 0.95 }, ], }, });

// Test catalog calling search
const catalogResponse = await tester.get('/api/search-parts?q=ABC123');

expect(catalogResponse.status).toBe(200);
expect(catalogResponse.json.data.results).toHaveLength(1);
expect(mockSearch.wasCalled('GET', '/api/search')).toBe(true);

});

test('should handle search service errors', async () => { const mockSearch = new MockServiceClient('http://localhost:3001'); mockSearch.mockGet('/api/search', { success: false, error: { code: 'SERVICE_ERROR', message: 'Search service unavailable', }, });

const response = await tester.get('/api/search-parts?q=ABC123');

expect(response.status).toBe(500);
expect(response.json.error.code).toBe('SEARCH_SERVICE_ERROR');

}); }); ```


E2E Testing

Docker-Based E2E Tests

```typescript // tests/e2e/part-search.test.ts import { describe, test, expect, beforeAll, afterAll } from 'bun:test';

describe('Part Search E2E', () => { beforeAll(async () => { // Ensure services are running // You might use docker-compose or check health endpoints });

test('complete search workflow', async () => { // 1. Search for part const searchResponse = await fetch('http://localhost:3001/api/search?q=ABC123'); const searchData = await searchResponse.json();

expect(searchData.success).toBe(true);
expect(searchData.data.results.length).toBeGreaterThan(0);

// 2. Get first result details from catalog
const partId = searchData.data.results[0].id;
const catalogResponse = await fetch(\`http://localhost:3003/api/parts/\${partId}\`);
const catalogData = await catalogResponse.json();

expect(catalogData.success).toBe(true);
expect(catalogData.data.partNumber).toBe('ABC123');

// 3. Check fitment
const fitmentResponse = await fetch(\`http://localhost:3002/api/fitment?partId=\${partId}\`);
const fitmentData = await fitmentResponse.json();

expect(fitmentData.success).toBe(true);
expect(fitmentData.data.equipment).toBeArray();

});

test('search with no results', async () => { const response = await fetch('http://localhost:3001/api/search?q=NONEXISTENT999'); const data = await response.json();

expect(data.success).toBe(true);
expect(data.data.results).toHaveLength(0);

}); }); ```

Running E2E Tests

```bash

Start all services

make dev

Wait for services to be healthy

sleep 10

Run E2E tests

bun test tests/e2e

Clean up

make down ```


Testing Best Practices

1. Test Naming

Good: ```typescript test('should return 404 when part not found', async () => { // Clear and specific }); ```

Bad: ```typescript test('test1', async () => { // Unclear what is being tested }); ```

2. Arrange-Act-Assert Pattern

```typescript test('should calculate total price', () => { // Arrange const cart = { items: [{ price: 10 }, { price: 20 }] };

// Act const total = calculateTotal(cart);

// Assert expect(total).toBe(30); }); ```

3. One Assertion Per Test (guideline, not rule)

```typescript // Prefer focused tests test('should return correct status code', async () => { const response = await tester.get('/api/parts'); expect(response.status).toBe(200); });

test('should return valid structure', async () => { const response = await tester.get('/api/parts'); expect(response.json).toHaveProperty('success'); expect(response.json).toHaveProperty('data'); }); ```

4. Test Independence

```typescript // BAD - tests depend on each other let createdPartId: string;

test('should create part', async () => { const response = await tester.post('/api/parts', { body: {...} }); createdPartId = response.json.data.id; // State shared between tests });

test('should get part', async () => { const response = await tester.get(`/api/parts/${createdPartId}`); // Fails if previous test fails });

// GOOD - tests are independent test('should create and retrieve part', async () => { // Create const createResponse = await tester.post('/api/parts', { body: {...} }); const partId = createResponse.json.data.id;

// Get const getResponse = await tester.get(`/api/parts/${partId}`); expect(getResponse.status).toBe(200); }); ```

5. Avoid Testing Implementation Details

```typescript // BAD - testing internal implementation test('should use redis cache', () => { const spy = jest.spyOn(redis, 'get'); getUser('123'); expect(spy).toHaveBeenCalled(); });

// GOOD - testing behavior test('should return user quickly on second request', async () => { const start1 = Date.now(); await getUser('123'); const time1 = Date.now() - start1;

const start2 = Date.now(); await getUser('123'); const time2 = Date.now() - start2;

expect(time2).toBeLessThan(time1); }); ```

6. Use Test Data Builders

```typescript // Use fixture functions const part = createMockPart({ partNumber: 'SPECIFIC123', manufacturer: 'TEST', });

// Or create builders for complex objects class PartBuilder { private part: Partial = {};

withPartNumber(pn: string) { this.part.partNumber = pn; return this; }

withManufacturer(mfr: string) { this.part.manufacturer = mfr; return this; }

build(): Part { return { id: this.part.id || 'default-id', partNumber: this.part.partNumber || 'DEFAULT', manufacturer: this.part.manufacturer || 'ACME', // ... other required fields with defaults }; } }

const part = new PartBuilder() .withPartNumber('ABC123') .withManufacturer('ACME') .build(); ```


CI/CD Integration

GitHub Actions Workflow

Our CI workflow (.github/workflows/ci.yml) runs:

  1. Lint - Biome check
  2. Type Check - TypeScript compilation
  3. Unit Tests - All package and service tests
  4. Integration Tests - With MongoDB and Elasticsearch
  5. Build - Ensure all services build

Test Requirements for PR Merge

  • All tests must pass
  • Test coverage must not decrease
  • No linting errors
  • TypeScript compilation must succeed

Running Tests Locally Like CI

```bash

Full CI check

bun run lint && bun test && bun run build

Or use script

bun run ci ```


Troubleshooting

Tests Failing Locally But Passing in CI

Cause: Different environments or cached data

Solution: ```bash

Clean install

rm -rf node_modules bun.lockb bun install

Clear test cache

rm -rf .bun-cache ```

Slow Tests

Cause: Too many integration/E2E tests or inefficient setup

Solution:

  • Add --timeout flag: bun test --timeout 10000
  • Run specific test files: bun test src/__tests__/fast.test.ts
  • Use test.only() during development
  • Optimize database seeds

Flaky Tests

Cause: Race conditions, timing issues, external dependencies

Solution:

  • Add proper await statements
  • Use beforeEach/afterEach for cleanup
  • Mock external services
  • Add retries for E2E tests (sparingly)

Mock Not Working

Cause: Import order or module caching

Solution: ```typescript // Ensure mocks are set up before imports import { MockServiceClient } from '@crop/shared-testing';

// Create mock before using const mock = new MockServiceClient('http://localhost:3001'); mock.mockGet('/api/search', { ... }); ```


Summary

This testing guide provides:

  • Clear testing strategy with test pyramid
  • Practical examples for unit, integration, and E2E tests
  • Testing utilities from @crop/shared-testing
  • Best practices for maintainable tests
  • CI/CD integration for automated testing
  • Troubleshooting common issues

Next Steps:

  1. Run bun test to see all tests passing
  2. Add tests for new features following this guide
  3. Review test coverage: bun test --coverage
  4. Set up CI/CD pipeline if not already configured

Need Help?

  • Check test examples in packages/shared-types/src/__tests__/
  • Review @crop/shared-testing utilities
  • See CI workflow in .github/workflows/ci.yml

On this page