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.

548 lines (481 loc) 15.8 kB
/** * TestCaseGenerator - Generate test cases from use case scenarios * * Transforms parsed use case scenarios into structured test case specifications * following TC-XXX format with test steps, expected results, and data requirements. * * @module src/testing/generators/test-case-generator * @implements @.aiwg/requirements/use-cases/UC-009-generate-test-artifacts.md * @architecture @.aiwg/architecture/software-architecture-doc.md - Section 5.3 TestGenerator * @nfr @.aiwg/requirements/nfr-modules/performance.md - NFR-TEST-01 (<10min for 100 reqs) * @tests @test/unit/testing/test-case-generator.test.ts * @depends @src/testing/generators/use-case-parser.ts * @agent @agentic/code/frameworks/sdlc-complete/agents/test-engineer.md */ import { UseCaseDocument, UseCaseScenario, UseCaseStep } from './use-case-parser.js'; // =========================== // Interfaces // =========================== export interface TestStep { number: number; action: string; expectedResult: string; testData?: string; } export interface TestCase { id: string; name: string; description: string; priority: 'critical' | 'high' | 'medium' | 'low'; type: 'positive' | 'negative' | 'boundary' | 'error'; level: 'unit' | 'integration' | 'e2e'; preconditions: string[]; steps: TestStep[]; postconditions: string[]; testData: TestDataRequirement[]; traceability: { useCaseId: string; scenarioId: string; nfrs: string[]; }; tags: string[]; estimatedDuration?: number; // milliseconds } export interface TestDataRequirement { name: string; type: string; constraints: string[]; examples: string[]; } export interface TestSuite { id: string; name: string; description: string; useCaseId: string; testCases: TestCase[]; coverage: { mainScenario: boolean; extensionsCovered: number; exceptionsCovered: number; total: number; }; generatedAt: string; } export interface GenerationOptions { includeNegativeTests?: boolean; includeBoundaryTests?: boolean; includeErrorTests?: boolean; testLevels?: Array<'unit' | 'integration' | 'e2e'>; maxTestsPerScenario?: number; generateDataRequirements?: boolean; } export interface GenerationResult { success: boolean; suite?: TestSuite; errors: string[]; warnings: string[]; stats: { totalTestCases: number; positiveTests: number; negativeTests: number; boundaryTests: number; errorTests: number; generationTimeMs: number; }; } // =========================== // TestCaseGenerator Class // =========================== export class TestCaseGenerator { private options: Required<GenerationOptions>; private testCounter: number = 0; constructor(options: GenerationOptions = {}) { this.options = { includeNegativeTests: true, includeBoundaryTests: true, includeErrorTests: true, testLevels: ['unit', 'integration', 'e2e'], maxTestsPerScenario: 10, generateDataRequirements: true, ...options }; } /** * Generate test suite from a use case document * * @param document - Parsed use case document * @returns Generation result with test suite */ generate(document: UseCaseDocument): GenerationResult { const startTime = Date.now(); const warnings: string[] = []; const testCases: TestCase[] = []; this.testCounter = 0; try { // Generate tests for main scenario const mainTests = this.generateScenarioTests( document.mainScenario, document, 'main' ); testCases.push(...mainTests); // Generate tests for extensions for (const extension of document.extensions) { const extTests = this.generateScenarioTests( extension, document, 'extension' ); testCases.push(...extTests); } // Generate tests for exceptions for (const exception of document.exceptions) { const excTests = this.generateScenarioTests( exception, document, 'exception' ); testCases.push(...excTests); } // Generate negative tests if enabled if (this.options.includeNegativeTests) { const negativeTests = this.generateNegativeTests(document); testCases.push(...negativeTests); } // Generate boundary tests if enabled if (this.options.includeBoundaryTests) { const boundaryTests = this.generateBoundaryTests(document); testCases.push(...boundaryTests); } const generationTimeMs = Date.now() - startTime; const suite: TestSuite = { id: `TS-${document.id}`, name: `Test Suite: ${document.title}`, description: `Auto-generated test suite for ${document.id}`, useCaseId: document.id, testCases, coverage: { mainScenario: true, extensionsCovered: document.extensions.length, exceptionsCovered: document.exceptions.length, total: testCases.length }, generatedAt: new Date().toISOString() }; return { success: true, suite, errors: [], warnings, stats: { totalTestCases: testCases.length, positiveTests: testCases.filter(t => t.type === 'positive').length, negativeTests: testCases.filter(t => t.type === 'negative').length, boundaryTests: testCases.filter(t => t.type === 'boundary').length, errorTests: testCases.filter(t => t.type === 'error').length, generationTimeMs } }; } catch (error: any) { return { success: false, errors: [`Generation failed: ${error.message}`], warnings, stats: { totalTestCases: 0, positiveTests: 0, negativeTests: 0, boundaryTests: 0, errorTests: 0, generationTimeMs: Date.now() - startTime } }; } } /** * Generate test cases from multiple use case documents * * @param documents - Array of parsed use case documents * @returns Map of use case ID to generation result */ generateBatch(documents: UseCaseDocument[]): Map<string, GenerationResult> { const results = new Map<string, GenerationResult>(); for (const doc of documents) { results.set(doc.id, this.generate(doc)); } return results; } // =========================== // Private Generation Methods // =========================== private generateScenarioTests( scenario: UseCaseScenario, document: UseCaseDocument, scenarioType: 'main' | 'extension' | 'exception' ): TestCase[] { const testCases: TestCase[] = []; // Generate one positive test for the happy path const positiveTest = this.createPositiveTest(scenario, document); testCases.push(positiveTest); // For extensions and exceptions, generate error handling tests if (scenarioType === 'exception' && this.options.includeErrorTests) { const errorTest = this.createErrorTest(scenario, document); testCases.push(errorTest); } return testCases; } private createPositiveTest( scenario: UseCaseScenario, document: UseCaseDocument ): TestCase { const testId = this.nextTestId(document.id); const level = this.determineTestLevel(scenario); const steps: TestStep[] = scenario.steps.map((step, index) => ({ number: index + 1, action: this.actionToTestAction(step), expectedResult: this.inferExpectedResult(step, scenario.steps[index + 1]), testData: this.inferTestData(step) })); const testData = this.options.generateDataRequirements ? this.extractDataRequirements(scenario) : []; return { id: testId, name: `${scenario.name} - Happy Path`, description: `Verify ${scenario.name} completes successfully`, priority: document.priority, type: 'positive', level, preconditions: [...document.preconditions, ...scenario.preconditions], steps, postconditions: [...document.postconditions, ...scenario.postconditions], testData, traceability: { useCaseId: document.id, scenarioId: scenario.id, nfrs: document.nfrs }, tags: [document.id, scenario.type, level], estimatedDuration: this.estimateDuration(steps.length, level) }; } private createErrorTest( scenario: UseCaseScenario, document: UseCaseDocument ): TestCase { const testId = this.nextTestId(document.id); const level = this.determineTestLevel(scenario); const steps: TestStep[] = [{ number: 1, action: `Trigger ${scenario.name} condition`, expectedResult: 'System handles error gracefully' }, { number: 2, action: 'Verify error handling', expectedResult: scenario.steps[0]?.action || 'Error is handled appropriately' }]; return { id: testId, name: `${scenario.name} - Error Handling`, description: `Verify system handles ${scenario.name} correctly`, priority: document.priority, type: 'error', level, preconditions: document.preconditions, steps, postconditions: [], testData: [], traceability: { useCaseId: document.id, scenarioId: scenario.id, nfrs: document.nfrs }, tags: [document.id, 'error', level], estimatedDuration: this.estimateDuration(steps.length, level) }; } private generateNegativeTests(document: UseCaseDocument): TestCase[] { const tests: TestCase[] = []; const mainScenario = document.mainScenario; // Generate negative test for invalid input if (mainScenario.steps.some(s => s.actor === 'user')) { const testId = this.nextTestId(document.id); tests.push({ id: testId, name: 'Invalid Input Handling', description: 'Verify system rejects invalid input', priority: 'high', type: 'negative', level: 'unit', preconditions: document.preconditions, steps: [{ number: 1, action: 'Provide invalid input', expectedResult: 'System displays validation error' }, { number: 2, action: 'Verify system state unchanged', expectedResult: 'No data corruption or state change' }], postconditions: ['System remains in valid state'], testData: [{ name: 'invalidInput', type: 'string', constraints: ['null', 'empty', 'malformed'], examples: ['null', '""', '<script>alert(1)</script>'] }], traceability: { useCaseId: document.id, scenarioId: `${document.id}-NEG`, nfrs: document.nfrs }, tags: [document.id, 'negative', 'validation'], estimatedDuration: 1000 }); } return tests; } private generateBoundaryTests(document: UseCaseDocument): TestCase[] { const tests: TestCase[] = []; // Generate boundary test for data limits const testId = this.nextTestId(document.id); tests.push({ id: testId, name: 'Boundary Value Testing', description: 'Verify system handles boundary values correctly', priority: 'medium', type: 'boundary', level: 'unit', preconditions: document.preconditions, steps: [{ number: 1, action: 'Test with minimum valid value', expectedResult: 'System accepts minimum value' }, { number: 2, action: 'Test with maximum valid value', expectedResult: 'System accepts maximum value' }, { number: 3, action: 'Test with value just below minimum', expectedResult: 'System rejects value' }, { number: 4, action: 'Test with value just above maximum', expectedResult: 'System rejects value' }], postconditions: [], testData: [{ name: 'boundaryValues', type: 'number', constraints: ['min', 'max', 'min-1', 'max+1'], examples: ['0', '100', '-1', '101'] }], traceability: { useCaseId: document.id, scenarioId: `${document.id}-BND`, nfrs: document.nfrs }, tags: [document.id, 'boundary', 'validation'], estimatedDuration: 2000 }); return tests; } // =========================== // Helper Methods // =========================== private nextTestId(useCaseId: string): string { this.testCounter++; return `TC-${useCaseId.replace('UC-', '')}-${String(this.testCounter).padStart(3, '0')}`; } private determineTestLevel(scenario: UseCaseScenario): 'unit' | 'integration' | 'e2e' { const stepCount = scenario.steps.length; const hasExternalActor = scenario.steps.some(s => s.actor === 'external'); if (hasExternalActor) return 'e2e'; if (stepCount > 5) return 'integration'; return 'unit'; } private actionToTestAction(step: UseCaseStep): string { const action = step.action; // Convert use case language to test language if (step.actor === 'user') { return action.replace(/^user\s*/i, 'User performs: '); } else if (step.actor === 'system') { return `Verify system ${action.toLowerCase()}`; } return action; } private inferExpectedResult(step: UseCaseStep, nextStep?: UseCaseStep): string { if (step.expectedResult) { return step.expectedResult; } // Infer from action const action = step.action.toLowerCase(); if (action.includes('display') || action.includes('show')) { return 'Correct information is displayed'; } if (action.includes('save') || action.includes('store')) { return 'Data is persisted successfully'; } if (action.includes('validate') || action.includes('verify')) { return 'Validation passes'; } if (action.includes('navigate') || action.includes('redirect')) { return 'User is redirected to correct page'; } if (action.includes('send') || action.includes('notify')) { return 'Notification is sent successfully'; } // Default based on next step if (nextStep) { return `System is ready for: ${nextStep.action}`; } return 'Action completes successfully'; } private inferTestData(step: UseCaseStep): string | undefined { const action = step.action.toLowerCase(); if (action.includes('enter') || action.includes('input')) { return 'Valid test input'; } if (action.includes('select') || action.includes('choose')) { return 'Valid selection'; } if (action.includes('upload')) { return 'Valid test file'; } return undefined; } private extractDataRequirements(scenario: UseCaseScenario): TestDataRequirement[] { const requirements: TestDataRequirement[] = []; for (const step of scenario.steps) { if (step.actor === 'user') { const action = step.action.toLowerCase(); if (action.includes('enter') || action.includes('input')) { requirements.push({ name: 'userInput', type: 'string', constraints: ['non-empty', 'valid format'], examples: ['test@example.com', 'Test User'] }); } if (action.includes('select') || action.includes('choose')) { requirements.push({ name: 'selection', type: 'option', constraints: ['valid option'], examples: ['option1', 'option2'] }); } } } return requirements; } private estimateDuration(stepCount: number, level: 'unit' | 'integration' | 'e2e'): number { const baseTime = { unit: 100, integration: 500, e2e: 2000 }; return baseTime[level] * stepCount; } }