feat: add .spec.ts generation for models, mappers, repositories and use-cases

Add 4 new Mustache templates for generating unit test specs:
- model-entity.spec.mustache: tests instantiation, property setting, mock builder
- mapper.spec.mustache: tests per-property DTO→Entity mapping, instanceof, all fields
- api.repository.impl.spec.mustache: tests HTTP method, response mapping, error propagation
- api.use-cases.impl.spec.mustache: tests repository delegation, observable forwarding

Generator changes:
- Add uppercaseHttpMethod to TagOperation for spec HTTP assertions
- Add testValue to TagOperationParam for auto-generated test arguments
- Add resolveTestParamValue utility for primitive/complex type test literals
- Add specs counter to GeneratedCount and GenerationReport
- Wire 4 new renderTemplate calls in schema and tag loops
- Update report generator to count .spec.ts files

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
didavila
2026-03-26 10:52:58 +01:00
parent 05a58c4254
commit 463626da0c
8 changed files with 343 additions and 5 deletions

View File

@@ -74,7 +74,8 @@ export function generateCleanArchitecture(
mappers: 0,
useCases: 0,
providers: 0,
mocks: 0
mocks: 0,
specs: 0
};
const schemas =
@@ -210,6 +211,26 @@ export function generateCleanArchitecture(
generatedCount,
'mocks'
);
// Model spec
renderTemplate(
templatesDir,
'model-entity.spec.mustache',
modelViewData,
path.join(outputDir, 'entities/models', `${toCamelCase(baseName)}.model.spec.ts`),
generatedCount,
'specs'
);
// Mapper spec
renderTemplate(
templatesDir,
'mapper.spec.mustache',
mapperViewData,
path.join(outputDir, 'data/mappers', `${toCamelCase(baseName)}.mapper.spec.ts`),
generatedCount,
'specs'
);
});
// 2. Generate Use Cases and Repositories from Paths/Tags
@@ -227,7 +248,8 @@ export function generateCleanArchitecture(
paramName: p.name,
dataType: mapSwaggerTypeToTs(p.schema?.type || ''),
description: p.description || '',
required: p.required
required: p.required,
testValue: resolveTestParamValue(mapSwaggerTypeToTs(p.schema?.type || ''))
}));
if (op.requestBody) {
@@ -241,7 +263,8 @@ export function generateCleanArchitecture(
paramName: 'body',
dataType: bodyType,
description: op.requestBody.description || '',
required: true
required: true,
testValue: resolveTestParamValue(bodyType)
});
}
@@ -265,6 +288,7 @@ export function generateCleanArchitecture(
summary: op.summary || '',
notes: op.description || '',
httpMethod: method.toLowerCase(),
uppercaseHttpMethod: method.toUpperCase(),
path: pathKey,
allParams: allParams.map((p, i: number) => ({
...p,
@@ -438,10 +462,30 @@ export function generateCleanArchitecture(
generatedCount,
'mocks'
);
// Repository impl spec
renderTemplate(
templatesDir,
'api.repository.impl.spec.mustache',
apiViewData,
path.join(outputDir, 'data/repositories', `${toCamelCase(tag)}.repository.impl.spec.ts`),
generatedCount,
'specs'
);
// Use-cases impl spec
renderTemplate(
templatesDir,
'api.use-cases.impl.spec.mustache',
apiViewData,
path.join(outputDir, 'domain/use-cases', `${toCamelCase(tag)}.use-cases.impl.spec.ts`),
generatedCount,
'specs'
);
});
logSuccess(
`${generatedCount.models} Models, ${generatedCount.repositories} Repos, ${generatedCount.useCases} Use Cases, ${generatedCount.mappers} Mappers, ${generatedCount.providers} Providers, ${generatedCount.mocks} Mocks generated`
`${generatedCount.models} Models, ${generatedCount.repositories} Repos, ${generatedCount.useCases} Use Cases, ${generatedCount.mappers} Mappers, ${generatedCount.providers} Providers, ${generatedCount.mocks} Mocks, ${generatedCount.specs} Specs generated`
);
return generatedCount;
}
@@ -463,3 +507,18 @@ function renderTemplate(
counter[key]++;
}
}
/** Resolves a simple test value literal for a given TypeScript type. */
function resolveTestParamValue(dataType: string): string {
switch (dataType) {
case 'string':
return "'test'";
case 'number':
return '1';
case 'boolean':
return 'true';
default:
if (dataType.endsWith('[]')) return '[]';
return '{} as any';
}
}

View File

@@ -12,6 +12,15 @@ function countMockFiles(dir: string): number {
}
}
/** Counts files ending with `.spec.ts` in a directory (returns 0 if directory does not exist). */
function countSpecFiles(dir: string): number {
try {
return fs.readdirSync(dir).filter((f) => f.endsWith('.spec.ts')).length;
} catch {
return 0;
}
}
/** Generates and persists the `generation-report.json` file with process statistics. */
export function generateReport(outputDir: string, analysis: SwaggerAnalysis): GenerationReport {
logStep('Generating report...');
@@ -35,7 +44,12 @@ export function generateReport(outputDir: string, analysis: SwaggerAnalysis): Ge
countMockFiles(path.join(outputDir, 'di/repositories')) +
countMockFiles(path.join(outputDir, 'di/use-cases')) +
countMockFiles(path.join(outputDir, 'domain/use-cases')) +
countMockFiles(path.join(outputDir, 'entities/models'))
countMockFiles(path.join(outputDir, 'entities/models')),
specs:
countSpecFiles(path.join(outputDir, 'entities/models')) +
countSpecFiles(path.join(outputDir, 'data/mappers')) +
countSpecFiles(path.join(outputDir, 'data/repositories')) +
countSpecFiles(path.join(outputDir, 'domain/use-cases'))
}
};