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
text/typescript
/**
* 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}'`;
}
}