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