Testing Standards and Guidelines
March 25, 2026 · View on GitHub
Overview
This document defines the testing standards, practices, and requirements for the Orderly codebase. All code must meet these standards to be considered production-ready.
Testing Philosophy
Core Principles
- High Coverage: Minimum 90% code coverage across all metrics
- Side-by-Side Tests: Test files live alongside implementation files
- AAA Pattern: Arrange, Act, Assert - no conditional logic in tests
- Fluent Style: Inline function calls to reduce code count
- Table-Driven Tests: Use
it.eachfor multiple test cases - Mock Isolation: All mocks defined in root describe, initialized in beforeEach
Test File Structure
Naming Convention
- Implementation file:
my-service.ts - Test file:
my-service.spec.ts(side-by-side)
Standard Test Template
import { MyService } from './my-service';
describe('MyService', () => {
// Mock values defined at root level
let mockDependency: jest.Mocked<DependencyType>;
let testValue: string;
let service: MyService;
beforeEach(() => {
// Initialize mocks and test values
mockDependency = {
method: jest.fn()
} as jest.Mocked<DependencyType>;
testValue = 'test-value';
service = new MyService(mockDependency);
});
afterEach(() => {
// Clean up mocks and resources
jest.clearAllMocks();
});
describe('publicMethod', () => {
it('should return expected result when input is valid', () => {
// Arrange (Apply)
mockDependency.method.mockReturnValue('mocked-result');
// Act (Action)
const result = service.publicMethod(testValue);
// Assert
expect(result).toBe('expected-result');
expect(mockDependency.method).toHaveBeenCalledWith(testValue);
});
it.each([
['input1', 'output1'],
['input2', 'output2'],
['input3', 'output3']
])('should transform %s to %s', (input, expected) => {
const result = service.publicMethod(input);
expect(result).toBe(expected);
});
});
describe('privateMethod (bracket notation)', () => {
it('should handle private logic correctly', () => {
// Access private method using bracket notation
const result = service['privateMethod']('test-input');
expect(result).toBeDefined();
});
});
});
Test Structure Rules
1. Root Describe
- Named after the file or class functionality
- Contains all mock declarations
- One class per file maximum
describe('ClassName', () => {
// All tests for this class
});
2. Function Describes
- One describe block per function
- Use bracket notation for private functions
describe('publicFunction', () => {
// Tests for public function
});
describe('privateFunction (bracket notation)', () => {
// Tests for private function
});
3. beforeEach Rules
- Initialize all mocks
- Set up test values
- Create service instances
- NO
jest.clearAllMocks()or reset calls
beforeEach(() => {
mockValue = createMock();
service = new Service(mockValue);
});
4. afterEach Rules
- Clear all mocks
- Clean up test resources
- Reset any global state
afterEach(() => {
jest.clearAllMocks();
// Clean up any test resources
});
5. Test Cases (AAA Pattern)
Arrange (Apply)
Set up mocks and test data:
mockDependency.method.mockReturnValue('value');
const input = 'test-input';
Act (Action)
Invoke the function under test:
const result = service.methodUnderTest(input);
Assert
Verify the results:
expect(result).toBe('expected');
expect(mockDependency.method).toHaveBeenCalledWith(input);
6. Table-Driven Tests
Use it.each for multiple similar test cases:
it.each([
// [input, expected, description]
['valid-input', true, 'valid case'],
['invalid-input', false, 'invalid case'],
['', false, 'empty case']
])('should handle %s correctly', (input, expected, description) => {
const result = service.validate(input);
expect(result).toBe(expected);
});
Coverage Requirements
Minimum Thresholds
{
"coverageThreshold": {
"global": {
"branches": 90,
"functions": 90,
"lines": 90,
"statements": 90
}
}
}
Coverage Reports
- HTML:
coverage/index.html- Visual coverage report - LCOV:
coverage/lcov.info- CI/CD integration - JSON:
coverage/coverage-final.json- Detailed metrics - Text: Console output for quick checks
Code Quality Standards
1. Cyclomatic Complexity
- Maximum per function: 10
- Average per file: 5
- Measured by SonarQube
2. Code Duplication
- Maximum allowed: 1%
- Measured by jscpd
- Enforced in test suite
3. Static Analysis (SonarQube)
- Maintainability Rating: A
- Reliability Rating: A
- Security Rating: A
- Code Smells: 0
- Bugs: 0
- Vulnerabilities: 0
Testing Best Practices
✅ DO
- Test public contracts: Focus on public API behavior
- Test edge cases: Empty strings, null, undefined, boundaries
- Test error cases: Exceptions and error conditions
- Use descriptive names: Test names explain what they verify
- Keep tests independent: No test should depend on another
- Mock external dependencies: Isolate the unit under test
- Use fluent assertions: Chain expect calls when appropriate
- Test one thing: Each test verifies one behavior
❌ DON'T
- No conditional logic: No if/else in test functions
- No loops: Use
it.eachinstead - No shared mutable state: Between test cases
- No test interdependence: Tests must run independently
- No implementation details: Test behavior, not implementation
- No magic values: Use named constants
- No duplicate setup: Use beforeEach for common setup
- No assertions in beforeEach: Only setup code
Example Test Files
Utility Class Test
// file-system-utils.spec.ts
import { FileSystemUtils } from './file-system-utils';
import * as fs from 'fs';
import * as path from 'node:path';
jest.mock('fs');
jest.mock('path');
describe('FileSystemUtils', () => {
const mockFs = fs as jest.Mocked<typeof fs>;
const mockPath = path as jest.Mocked<typeof path>;
let testPath: string;
let testContent: string;
beforeEach(() => {
testPath = '/test/path/file.txt';
testContent = 'test content';
});
afterEach(() => {
jest.clearAllMocks();
});
describe('exists', () => {
it.each([
[true, 'exists'],
[false, 'does not exist']
])('should return %s when file %s', expected => {
mockFs.existsSync.mockReturnValue(expected);
const result = FileSystemUtils.exists(testPath);
expect(result).toBe(expected);
expect(mockFs.existsSync).toHaveBeenCalledWith(testPath);
});
});
describe('readFile', () => {
it('should read and return file content', () => {
mockFs.readFileSync.mockReturnValue(testContent);
const result = FileSystemUtils.readFile(testPath);
expect(result).toBe(testContent);
expect(mockFs.readFileSync).toHaveBeenCalledWith(testPath, 'utf8');
});
});
});
Service Class Test
// config-loader.spec.ts
import { ConfigLoader } from './config-loader';
import { FileSystemUtils } from '../utils/file-system-utils';
import { ConfigParser } from '../utils/config-parser';
jest.mock('../utils/file-system-utils');
jest.mock('../utils/config-parser');
describe('ConfigLoader', () => {
const mockFileSystemUtils = FileSystemUtils as jest.Mocked<typeof FileSystemUtils>;
const mockConfigParser = ConfigParser as jest.Mocked<typeof ConfigParser>;
let testConfigPath: string;
let testConfig: any;
beforeEach(() => {
testConfigPath = '/config/test.yml';
testConfig = { test: 'config' };
});
afterEach(() => {
jest.clearAllMocks();
});
describe('load', () => {
it('should load config from specified path', () => {
mockFileSystemUtils.exists.mockReturnValue(true);
mockConfigParser.parse.mockReturnValue(testConfig);
const result = ConfigLoader.load(testConfigPath);
expect(result).toMatchObject(testConfig);
expect(mockFileSystemUtils.exists).toHaveBeenCalledWith(testConfigPath);
expect(mockConfigParser.parse).toHaveBeenCalledWith(testConfigPath);
});
it('should throw error when config file not found', () => {
mockFileSystemUtils.exists.mockReturnValue(false);
expect(() => ConfigLoader.load(testConfigPath)).toThrow('Config file not found');
});
});
});
Configuration Files
jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/*.spec.ts'],
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.spec.ts', '!src/index.ts'],
coverageThreshold: {
global: {
branches: 90,
functions: 90,
lines: 90,
statements: 90
}
},
coverageReporters: ['text', 'lcov', 'html', 'json'],
verbose: true
};
tsconfig.test.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["jest", "node"]
},
"include": ["src/**/*.ts", "src/**/*.spec.ts"]
}
NPM Scripts
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --runInBand --coverage",
"test:ci": "jest --coverage --ci --maxWorkers=2",
"lint": "eslint src",
"lint:fix": "eslint src --fix",
"format": "prettier --write src",
"format:check": "prettier --check src",
"quality": "npm run lint && npm run format:check && npm run test:coverage",
"duplication": "jscpd src --threshold 1",
"complexity": "sonarqube-scanner",
"verify": "npm run quality && npm run duplication"
}
}
Quality Gate Enforcement
All quality checks are enforced as part of the test suite:
- Unit Tests: Must pass with 90% coverage
- Linting: No ESLint errors
- Formatting: Code must be formatted
- Duplication: < 1% duplicated code
- Complexity: Average cyclomatic complexity < 5
Continuous Integration
Tests are run in CI/CD pipeline with:
- Full coverage report
- Quality gate checks
- Duplication detection
- Static analysis
Failing any check blocks the build.
Summary
This testing standard ensures:
- ✅ High code quality (90% coverage minimum)
- ✅ Maintainable tests (clear structure)
- ✅ Fast feedback (isolated tests)
- ✅ Reliable builds (quality gates)
- ✅ Clean code (duplication < 1%)
- ✅ Low complexity (maintainable)
Follow these standards for all new code and when refactoring existing code.