Testing Guide for KLA Digital Services

Overview

This guide explains how to write and run tests for services in the KLA Digital monorepo. All services must maintain ≥80% test coverage across lines, functions, branches, and statements.

Test Structure

Unit Tests

Unit tests should be placed alongside the code they test with the naming convention *.test.ts or *.spec.ts.

Example structure:

services/my-service/
├── src/
│   ├── index.ts
│   ├── index.test.ts
│   ├── utils/
│   │   ├── helper.ts
│   │   └── helper.test.ts

Integration Tests

Integration tests should use the naming convention *.integration.test.ts and test real interactions between components.

Writing Tests

Basic Test Template

import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';

describe('MyComponent', () => {
  let originalEnv: NodeJS.ProcessEnv;

  beforeEach(() => {
    // Save original state
    originalEnv = { ...process.env };
    // Setup test environment
    jest.clearAllMocks();
  });

  afterEach(() => {
    // Restore original state
    process.env = originalEnv;
  });

  it('should perform expected behavior', () => {
    // Arrange
    const input = 'test';

    // Act
    const result = myFunction(input);

    // Assert
    expect(result).toBe('expected');
  });
});

Mocking Best Practices

  1. Mock external dependencies - Always mock external services, APIs, and modules
  2. Use jest.mock() - Place mocks at the top of the test file
  3. Clear mocks between tests - Use jest.clearAllMocks() in beforeEach
  4. Verify mock calls - Use expect(mockFn).toHaveBeenCalledWith()

Example:

// Mock modules before imports
jest.mock('@opentelemetry/api');
jest.mock('./external-service');

import { trace } from '@opentelemetry/api';
import { externalService } from './external-service';

describe('Service with mocks', () => {
  const mockTrace = trace as jest.Mocked<typeof trace>;
  const mockService = externalService as jest.Mocked<typeof externalService>;

  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('should call external service', async () => {
    mockService.getData.mockResolvedValue({ data: 'test' });

    const result = await myFunction();

    expect(mockService.getData).toHaveBeenCalledTimes(1);
    expect(result).toEqual({ data: 'test' });
  });
});

Running Tests

Commands

# Run all tests
pnpm test

# Run tests in watch mode
pnpm test:watch

# Run tests with coverage
pnpm test:coverage

# Run integration tests only
pnpm test:integration

# Run tests for a specific service
cd services/my-service && pnpm test

Coverage Requirements

The CI pipeline enforces the following coverage thresholds: - Lines: ≥80% - Functions: ≥80% - Branches: ≥80% - Statements: ≥80%

Coverage reports are generated in services/*/coverage/ directories.

Policy Testing

Cerbos Policy Tests

Cerbos policies should have corresponding test files with the _test.yaml suffix.

Example test structure:

name: PolicyTestSuite
description: Tests for my_resource policy

tests:
  - name: test_user_can_read
    input:
      actions: ["read"]
      resource:
        kind: "my_resource"
        id: "123"
      principal:
        id: "user1"
        roles: ["viewer"]
    expected:
      - action: "read"
        effect: EFFECT_ALLOW

Kyverno Policy Tests

Kyverno policies should have test files in policies/kyverno/tests/ with the -test.yaml suffix.

Example test structure:

apiVersion: cli.kyverno.io/v1alpha1
kind: Test
metadata:
  name: my-policy-test
policies:
  - ../my-policy.yaml
resources:
  - ../test-resources/valid-resource.yaml
  - ../test-resources/invalid-resource.yaml
results:
  - policy: my-policy
    rule: my-rule
    resource: valid-resource
    result: pass
  - policy: my-policy
    rule: my-rule
    resource: invalid-resource
    result: fail

Debugging Tests

Run specific test file

pnpm test path/to/test.ts

Run tests matching pattern

pnpm test --testNamePattern="should handle errors"

Debug with VS Code

Add this configuration to .vscode/launch.json:

{
  "type": "node",
  "request": "launch",
  "name": "Jest Debug",
  "program": "${workspaceFolder}/node_modules/.bin/jest",
  "args": ["--runInBand", "--no-cache", "${file}"],
  "console": "integratedTerminal",
  "internalConsoleOptions": "neverOpen"
}

Common Pitfalls

  1. Forgetting to restore state - Always restore environment variables and global state
  2. Not mocking timers - Use jest.useFakeTimers() for time-dependent tests
  3. Incomplete async handling - Always await promises and use async/await
  4. Not testing error cases - Test both success and failure scenarios
  5. Over-mocking - Only mock what's necessary; prefer integration tests when possible

Adding Tests to New Services

When creating a new service:

  1. Copy the jest.config.js from an existing service
  2. Add test scripts to package.json
  3. Create initial test files for all modules
  4. Run coverage to ensure ≥80% threshold
  5. Add to CI pipeline if needed

Example jest.config.js:

module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
  testPathIgnorePatterns: ['/node_modules/', '\\.integration\\.test\\.ts$'],
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/**/*.d.ts',
    '!src/**/*.test.{ts,tsx}',
    '!src/**/__tests__/**',
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
};