Skip to main content

Testing Guide

Comprehensive testing is crucial for maintaining the quality and reliability of CI Dokumentor. This guide covers all aspects of testing in the project.

Testing Philosophy

CI Dokumentor follows these testing principles:

  1. Test-Driven Development - Write tests before or alongside implementation
  2. Clean Test Code - Tests should be as clean and maintainable as production code
  3. Fast Feedback - Tests should run quickly to enable rapid development
  4. Comprehensive Coverage - Aim for high test coverage across all packages
  5. Real-World Scenarios - Tests should reflect actual usage patterns

Test Stack

CI Dokumentor uses a modern testing stack:

Test Types

Unit Tests

Fast, isolated tests focusing on individual components:

# Run all unit tests
pnpm test

# Run tests for specific package
pnpm nx test core
pnpm nx test cli
pnpm nx test repository-github

# Run tests for specific file
pnpm nx test core src/formatter/markdown/markdown-formatter.adapter.spec.ts

Integration Tests

Tests that verify component interactions and realistic usage patterns:

  • End-to-end CLI testing (.e2e.spec.ts)
  • Generator adapter integration tests
  • Migration adapter integration tests

Mock Testing

Comprehensive mocking strategies using dependency injection:

// Example: Mocking repository providers
beforeEach(() => {
container
.rebind(TYPES.RepositoryProvider)
.toConstantValue(mockRepositoryProvider);
});

Running Tests

Workspace Commands

# Run all tests across all packages
pnpm test

# Run tests with coverage
pnpm test:ci

# Run tests in watch mode (development)
pnpm nx test core --watch

Package-Specific Commands

# Core package tests
pnpm nx test core

# CLI package tests
pnpm nx test cli

# GitHub Actions package tests
pnpm nx test cicd-github-actions

# Repository providers
pnpm nx test repository-git
pnpm nx test repository-github

Coverage Reports

# Generate coverage reports
pnpm test:ci

# View coverage in browser (after running coverage)
open coverage/index.html

Testing Guidelines

Writing Good Tests

Test Structure (AAA Pattern)

describe("Component", () => {
it("should behavior when condition", () => {
// Arrange - Setup test data and mocks
const input = "test-input";
const expectedOutput = "expected-result";

// Act - Execute the code under test
const result = systemUnderTest.method(input);

// Assert - Verify the results
expect(result).toBe(expectedOutput);
});
});

Descriptive Test Names

Use clear, descriptive test names that explain the behavior:

// ✅ Good: describes behavior and context
it("should throw error when source file does not exist");

// ❌ Bad: vague and unclear
it("should work");
it("test error case");

Test Dependencies and Mocking

Use dependency injection for clean, testable code:

// Example from core package
beforeEach(() => {
container = new Container();
container.bind(TYPES.FormatterAdapter).to(MockFormatterAdapter);
});

File System Testing

Use mock-fs for End-to-end file system operations tests only:

import mockFs from "mock-fs";

beforeEach(() => {
mockFs({
"/test": {
"action.yml": "name: Test Action",
"README.md": "# Test",
},
});
});

afterEach(() => {
mockFs.restore();
});

CI/CD Testing Patterns

Generator Adapter Testing

Test documentation generation with real manifest files:

it("should generate complete documentation", async () => {
// Arrange: Real action.yml content
const actionContent = `
name: My Action
description: Test action
inputs:
test-input:
description: Test input
`;

// Act: Generate documentation
await generator.generateDocumentation({
source: "/test/action.yml",
// ... other parameters
});

// Assert: Verify generated content
const result = readFileSync("/test/README.md", "utf-8");
expect(result).toContain("## Inputs");
});

Migration Testing

Test migration adapters with before/after scenarios:

it("should migrate action-docs markers", async () => {
const before = `
<!-- action-docs-inputs source="action.yml" -->
<!-- action-docs-outputs source="action.yml" -->
`;

const expected = `
<!-- inputs:start -->
<!-- inputs:end -->
<!-- outputs:start -->
<!-- outputs:end -->
`;

const result = await migrationAdapter.migrate(before);
expect(result).toBe(expected);
});

Common Testing Patterns

Repository Provider Testing

describe("GitHubRepositoryProvider", () => {
it("should support GitHub repositories", async () => {
// Mock Git remote URL
mockGitRemote("https://github.com/owner/repo.git");

const supports = await provider.supports();
expect(supports).toBe(true);
});
});

CLI Testing

describe("CLI", () => {
it("should generate documentation with valid options", async () => {
const result = await cli.run([
"generate",
"--source",
"/test/action.yml",
"--dry-run",
]);

expect(result.exitCode).toBe(0);
});
});

Performance Testing

Benchmarking

For performance-critical operations:

it("should parse large action files efficiently", () => {
const start = performance.now();

parser.parse(largeActionContent);

const duration = performance.now() - start;
expect(duration).toBeLessThan(100); // ms
});

Troubleshooting Tests

Common Issues

File System Permissions

// Ensure mock-fs is properly restored
afterEach(() => {
mockFs.restore();
});

Dependency Injection

// Verify container bindings
beforeEach(() => {
container.isBound(TYPES.SomeService)
? container.rebind(TYPES.SomeService)
: container.bind(TYPES.SomeService);
});

Async Operations

// Always await async operations in tests
await expect(async () => {
await service.methodThatThrows();
}).rejects.toThrow("Expected error");

Debug Tests

# Run single test file with debug output
pnpm nx test core --testNamePattern="specific test name"

# Run with verbose output
pnpm nx test core --verbose