UNPKG

aiwg

Version:

Cognitive architecture for AI-augmented software development with structured memory, ensemble validation, and closed-loop correction. FAIR-aligned artifacts, 84% cost reduction via human-in-the-loop, standards adopted by 100+ organizations.

485 lines (413 loc) 14.2 kB
/** * TestCodeGenerator - Generate test code scaffolds from test cases * * Generates test file skeletons (unit, integration, E2E) implementing * the arrange-act-assert pattern with TODO comments for manual completion. * * @module src/testing/generators/test-code-generator */ import { TestCase, TestSuite, TestStep } from './test-case-generator.js'; // =========================== // Interfaces // =========================== export interface CodeGeneratorOptions { framework: 'vitest' | 'jest' | 'mocha' | 'playwright'; language: 'typescript' | 'javascript'; includeSetup?: boolean; includeTeardown?: boolean; includeComments?: boolean; indentSize?: number; generateMocks?: boolean; } export interface GeneratedFile { filename: string; content: string; testLevel: 'unit' | 'integration' | 'e2e'; testCount: number; } export interface CodeGenerationResult { success: boolean; files: GeneratedFile[]; errors: string[]; warnings: string[]; stats: { totalFiles: number; totalTests: number; unitTests: number; integrationTests: number; e2eTests: number; }; } // =========================== // TestCodeGenerator Class // =========================== export class TestCodeGenerator { private options: Required<CodeGeneratorOptions>; constructor(options: CodeGeneratorOptions) { this.options = { includeSetup: true, includeTeardown: true, includeComments: true, indentSize: 2, generateMocks: true, ...options }; } /** * Generate test files from a test suite * * @param suite - Test suite with test cases * @returns Code generation result with generated files */ generate(suite: TestSuite): CodeGenerationResult { const warnings: string[] = []; const files: GeneratedFile[] = []; try { // Group test cases by level const unitTests = suite.testCases.filter(tc => tc.level === 'unit'); const integrationTests = suite.testCases.filter(tc => tc.level === 'integration'); const e2eTests = suite.testCases.filter(tc => tc.level === 'e2e'); // Generate unit test file if (unitTests.length > 0) { const unitFile = this.generateTestFile(unitTests, suite, 'unit'); files.push(unitFile); } // Generate integration test file if (integrationTests.length > 0) { const integrationFile = this.generateTestFile(integrationTests, suite, 'integration'); files.push(integrationFile); } // Generate E2E test file if (e2eTests.length > 0) { const e2eFile = this.generateE2ETestFile(e2eTests, suite); files.push(e2eFile); } return { success: true, files, errors: [], warnings, stats: { totalFiles: files.length, totalTests: suite.testCases.length, unitTests: unitTests.length, integrationTests: integrationTests.length, e2eTests: e2eTests.length } }; } catch (error: any) { return { success: false, files, errors: [`Code generation failed: ${error.message}`], warnings, stats: { totalFiles: 0, totalTests: 0, unitTests: 0, integrationTests: 0, e2eTests: 0 } }; } } /** * Generate a test file for unit or integration tests */ private generateTestFile( testCases: TestCase[], suite: TestSuite, level: 'unit' | 'integration' ): GeneratedFile { const indent = ' '.repeat(this.options.indentSize); const ext = this.options.language === 'typescript' ? '.ts' : '.js'; const filename = `${suite.useCaseId.toLowerCase()}.${level}.test${ext}`; const lines: string[] = []; // File header lines.push(this.generateFileHeader(suite, level)); lines.push(''); // Imports lines.push(this.generateImports(level)); lines.push(''); // Test suite lines.push(`describe('${suite.name} - ${level.charAt(0).toUpperCase() + level.slice(1)} Tests', () => {`); // Setup/teardown if (this.options.includeSetup) { lines.push(this.generateSetup(indent, suite)); } if (this.options.includeTeardown) { lines.push(this.generateTeardown(indent)); } // Test cases for (const testCase of testCases) { lines.push(''); lines.push(this.generateTestCase(testCase, indent)); } lines.push('});'); lines.push(''); return { filename, content: lines.join('\n'), testLevel: level, testCount: testCases.length }; } /** * Generate an E2E test file (Playwright format) */ private generateE2ETestFile(testCases: TestCase[], suite: TestSuite): GeneratedFile { const indent = ' '.repeat(this.options.indentSize); const ext = this.options.language === 'typescript' ? '.ts' : '.js'; const filename = `${suite.useCaseId.toLowerCase()}.e2e.test${ext}`; const lines: string[] = []; // File header lines.push(this.generateFileHeader(suite, 'e2e')); lines.push(''); // Playwright imports if (this.options.framework === 'playwright') { lines.push("import { test, expect } from '@playwright/test';"); } else { lines.push(this.generateImports('e2e')); } lines.push(''); // Test suite lines.push(`test.describe('${suite.name} - E2E Tests', () => {`); // Setup if (this.options.includeSetup) { lines.push(`${indent}test.beforeEach(async ({ page }) => {`); lines.push(`${indent}${indent}// TODO: Navigate to application and set up initial state`); lines.push(`${indent}${indent}await page.goto('/');`); lines.push(`${indent}});`); lines.push(''); } // Test cases for (const testCase of testCases) { lines.push(this.generateE2ETestCase(testCase, indent)); lines.push(''); } lines.push('});'); lines.push(''); return { filename, content: lines.join('\n'), testLevel: 'e2e', testCount: testCases.length }; } /** * Generate file header with metadata */ private generateFileHeader(suite: TestSuite, _level: string): string { if (!this.options.includeComments) return ''; return `/** * ${_level.toUpperCase()} Tests: ${suite.name} * * Auto-generated from ${suite.useCaseId} * Generated at: ${suite.generatedAt} * * @module test/${_level}/${suite.useCaseId.toLowerCase()} */`; } /** * Generate imports based on framework */ private generateImports(_level: 'unit' | 'integration' | 'e2e'): string { switch (this.options.framework) { case 'vitest': return "import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';"; case 'jest': return "import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';"; case 'mocha': return "import { describe, it, before, after } from 'mocha';\nimport { expect } from 'chai';"; case 'playwright': return "import { test, expect } from '@playwright/test';"; default: return ''; } } /** * Generate setup block */ private generateSetup(indent: string, _suite: TestSuite): string { const lines: string[] = []; const mockFn = this.options.framework === 'vitest' ? 'vi.fn()' : 'jest.fn()'; lines.push(`${indent}// Test fixtures`); if (this.options.language === 'typescript') { lines.push(`${indent}let testContext: Record<string, any>;`); } else { lines.push(`${indent}let testContext;`); } lines.push(''); lines.push(`${indent}beforeEach(() => {`); lines.push(`${indent}${indent}// Arrange: Set up test context`); lines.push(`${indent}${indent}testContext = {`); lines.push(`${indent}${indent}${indent}// TODO: Add test fixtures`); lines.push(`${indent}${indent}};`); if (this.options.generateMocks) { lines.push(''); lines.push(`${indent}${indent}// TODO: Set up mocks`); lines.push(`${indent}${indent}// const mockDependency = ${mockFn};`); } lines.push(`${indent}});`); return lines.join('\n'); } /** * Generate teardown block */ private generateTeardown(indent: string): string { const lines: string[] = []; lines.push(''); lines.push(`${indent}afterEach(() => {`); lines.push(`${indent}${indent}// TODO: Clean up after each test`); if (this.options.framework === 'vitest') { lines.push(`${indent}${indent}vi.clearAllMocks();`); } else if (this.options.framework === 'jest') { lines.push(`${indent}${indent}jest.clearAllMocks();`); } lines.push(`${indent}});`); return lines.join('\n'); } /** * Generate a single test case */ private generateTestCase(testCase: TestCase, indent: string): string { const lines: string[] = []; const indent2 = indent + indent; const indent3 = indent2 + indent; // Test description comment if (this.options.includeComments) { lines.push(`${indent}/**`); lines.push(`${indent} * ${testCase.id}: ${testCase.name}`); lines.push(`${indent} * ${testCase.description}`); lines.push(`${indent} * Type: ${testCase.type}`); lines.push(`${indent} * Priority: ${testCase.priority}`); lines.push(`${indent} */`); } // Test function lines.push(`${indent}it('${this.escapeString(testCase.name)}', async () => {`); // Preconditions if (testCase.preconditions.length > 0) { lines.push(`${indent2}// Preconditions:`); for (const pre of testCase.preconditions) { lines.push(`${indent2}// - ${pre}`); } lines.push(''); } // Arrange lines.push(`${indent2}// Arrange`); if (testCase.testData.length > 0) { lines.push(`${indent2}const testData = {`); for (const data of testCase.testData) { lines.push(`${indent3}${data.name}: ${this.getExampleValue(data.examples)}, // ${data.type}: ${data.constraints.join(', ')}`); } lines.push(`${indent2}};`); } else { lines.push(`${indent2}// TODO: Set up test data`); } lines.push(''); // Act lines.push(`${indent2}// Act`); for (const step of testCase.steps) { if (step.action.toLowerCase().includes('verify') || step.action.toLowerCase().includes('check')) { continue; // Skip verification steps for Act section } lines.push(`${indent2}// Step ${step.number}: ${step.action}`); lines.push(`${indent2}// TODO: Implement action`); } lines.push(''); // Assert lines.push(`${indent2}// Assert`); for (const step of testCase.steps) { if (!step.action.toLowerCase().includes('verify') && !step.action.toLowerCase().includes('check')) { continue; // Skip action steps for Assert section } lines.push(`${indent2}// ${step.expectedResult}`); lines.push(`${indent2}// expect(result).toEqual(expected);`); } // Default assertion if no verify steps const hasVerifySteps = testCase.steps.some(s => s.action.toLowerCase().includes('verify') || s.action.toLowerCase().includes('check') ); if (!hasVerifySteps) { lines.push(`${indent2}// TODO: Add assertions`); lines.push(`${indent2}expect(true).toBe(true); // Placeholder`); } // Postconditions if (testCase.postconditions.length > 0) { lines.push(''); lines.push(`${indent2}// Postconditions:`); for (const post of testCase.postconditions) { lines.push(`${indent2}// - ${post}`); } } lines.push(`${indent}});`); return lines.join('\n'); } /** * Generate E2E test case (Playwright format) */ private generateE2ETestCase(testCase: TestCase, indent: string): string { const lines: string[] = []; const indent2 = indent + indent; // Test description comment if (this.options.includeComments) { lines.push(`${indent}/**`); lines.push(`${indent} * ${testCase.id}: ${testCase.name}`); lines.push(`${indent} * ${testCase.description}`); lines.push(`${indent} */`); } // Test function lines.push(`${indent}test('${this.escapeString(testCase.name)}', async ({ page }) => {`); // Steps for (const step of testCase.steps) { lines.push(`${indent2}// Step ${step.number}: ${step.action}`); lines.push(this.generateE2EStepCode(step, indent2)); lines.push(''); } // Final assertion lines.push(`${indent2}// TODO: Add final assertions`); lines.push(`${indent2}await expect(page).toHaveURL(/.*expected/);`); lines.push(`${indent}});`); return lines.join('\n'); } /** * Generate code for an E2E step */ private generateE2EStepCode(step: TestStep, indent: string): string { const action = step.action.toLowerCase(); if (action.includes('click') || action.includes('press')) { return `${indent}await page.click('selector'); // TODO: Update selector`; } if (action.includes('enter') || action.includes('type') || action.includes('input')) { return `${indent}await page.fill('selector', '${step.testData || 'test-data'}'); // TODO: Update`; } if (action.includes('navigate') || action.includes('go to')) { return `${indent}await page.goto('/path'); // TODO: Update path`; } if (action.includes('wait')) { return `${indent}await page.waitForSelector('selector'); // TODO: Update selector`; } if (action.includes('verify') || action.includes('check') || action.includes('see')) { return `${indent}await expect(page.locator('selector')).toBeVisible(); // ${step.expectedResult}`; } return `${indent}// TODO: Implement step - ${step.action}`; } /** * Escape string for use in test names */ private escapeString(str: string): string { return str.replace(/'/g, "\\'").replace(/"/g, '\\"'); } /** * Get example value from test data */ private getExampleValue(examples: string[]): string { if (examples.length === 0) return "'test-value'"; const example = examples[0]; if (example.match(/^\d+$/)) return example; return `'${example}'`; } }