Skip to content

Commit 7f7084f

Browse files
bcallaghan-etBen Callaghan
andauthored
feat(core): add test project generator (#69)
* feat(core): add test project generator Add a generator that creates test projects for existing apps/libs Co-authored-by: Ben Callaghan <bcallaghan@selectbankcard.com>
1 parent cfc2e20 commit 7f7084f

File tree

11 files changed

+402
-77
lines changed

11 files changed

+402
-77
lines changed

packages/core/generators.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@
3939
"factory": "./src/generators/restore/generator",
4040
"schema": "./src/generators/restore/schema.json",
4141
"description": "Restores NuGet packages and .NET tools used by the workspace"
42+
},
43+
"test": {
44+
"factory": "./src/generators/test/generator",
45+
"schema": "./src/generators/test/schema.json",
46+
"description": "Generate a .NET test project for an existing application or library"
4247
}
4348
}
4449
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Tree } from '@nrwl/devkit';
2+
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
3+
4+
import { DotNetClient, mockDotnetFactory } from '@nx-dotnet/dotnet';
5+
6+
import * as mockedProjectGenerator from '../utils/generate-test-project';
7+
import generator from './generator';
8+
import { NxDotnetGeneratorSchema } from './schema';
9+
10+
jest.mock('../utils/generate-test-project');
11+
12+
describe('nx-dotnet test generator', () => {
13+
let appTree: Tree;
14+
let dotnetClient: DotNetClient;
15+
16+
const options: NxDotnetGeneratorSchema = {
17+
project: 'existing',
18+
testTemplate: 'xunit',
19+
language: 'C#',
20+
skipOutputPathManipulation: true,
21+
};
22+
23+
beforeEach(() => {
24+
appTree = createTreeWithEmptyWorkspace();
25+
dotnetClient = new DotNetClient(mockDotnetFactory());
26+
});
27+
28+
it('should run successfully', async () => {
29+
await generator(appTree, options, dotnetClient);
30+
});
31+
32+
it('should call project generator with application project type', async () => {
33+
const projectGenerator = (mockedProjectGenerator as jest.Mocked<
34+
typeof mockedProjectGenerator
35+
>).GenerateTestProject;
36+
37+
await generator(appTree, options, dotnetClient);
38+
expect(projectGenerator).toHaveBeenCalledWith(
39+
appTree,
40+
options,
41+
dotnetClient,
42+
);
43+
});
44+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Tree } from '@nrwl/devkit';
2+
3+
import { DotNetClient, dotnetFactory } from '@nx-dotnet/dotnet';
4+
5+
import { GenerateTestProject } from '../utils/generate-test-project';
6+
import { NxDotnetGeneratorSchema } from './schema';
7+
8+
export default function (
9+
host: Tree,
10+
options: NxDotnetGeneratorSchema,
11+
dotnetClient = new DotNetClient(dotnetFactory()),
12+
) {
13+
return GenerateTestProject(host, options, dotnetClient);
14+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { NxDotnetTestGeneratorSchema } from '../../models';
2+
3+
export type NxDotnetGeneratorSchema = NxDotnetTestGeneratorSchema;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"$schema": "http://json-schema.org/schema",
3+
"cli": "nx",
4+
"id": "@nx-dotnet/core:lib",
5+
"title": "NxDotnet Test Generator",
6+
"description": "Generate a .NET test project for an existing application or library",
7+
"type": "object",
8+
"properties": {
9+
"project": {
10+
"type": "string",
11+
"description": "The existing project to generate tests for",
12+
"$default": {
13+
"$source": "argv",
14+
"index": 0
15+
}
16+
},
17+
"testTemplate": {
18+
"type": "string",
19+
"description": "Which template should be used for creating the tests project?",
20+
"default": "nunit",
21+
"enum": ["nunit", "xunit", "mstest"],
22+
"x-prompt": {
23+
"message": "Which template should be used for creating the tests project",
24+
"type": "list",
25+
"items": [
26+
{ "value": "nunit", "label": "NUnit 3 Test Project" },
27+
{ "value": "xunit", "label": "xUnit Test Project" },
28+
{ "value": "mstest", "label": "Unit Test Project" }
29+
]
30+
}
31+
},
32+
"language": {
33+
"type": "string",
34+
"description": "Which language should the project use?",
35+
"x-prompt": {
36+
"message": "Which language should the project use?",
37+
"type": "list",
38+
"items": ["C#", "F#", "VB"]
39+
}
40+
},
41+
"skipOutputPathManipulation": {
42+
"type": "boolean",
43+
"description": "Skip XML changes for default build path",
44+
"default": false
45+
}
46+
},
47+
"required": ["name", "testTemplate"]
48+
}

packages/core/src/generators/utils/generate-project.spec.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,6 @@ describe('nx-dotnet project generator', () => {
5959
expect(config.targets.serve).not.toBeDefined();
6060
});
6161

62-
it('should tag generated projects', async () => {
63-
await GenerateProject(appTree, options, dotnetClient, 'library');
64-
const config = readProjectConfiguration(appTree, 'test');
65-
expect(config.tags).toContain('nx-dotnet');
66-
});
67-
6862
it('should run successfully for applications', async () => {
6963
await GenerateProject(appTree, options, dotnetClient, 'application');
7064
const config = readProjectConfiguration(appTree, 'test');
@@ -102,13 +96,6 @@ describe('nx-dotnet project generator', () => {
10296
expect(config.targets.lint).toBeDefined();
10397
});
10498

105-
it('should include lint target in test project', async () => {
106-
options.testTemplate = 'nunit';
107-
await GenerateProject(appTree, options, dotnetClient, 'application');
108-
const config = readProjectConfiguration(appTree, 'test-test');
109-
expect(config.targets.lint).toBeDefined();
110-
});
111-
11299
it('should prepend directory name to project name', async () => {
113100
options.directory = 'sub-dir';
114101
const spy = spyOn(dotnetClient, 'new');

packages/core/src/generators/utils/generate-project.ts

Lines changed: 30 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
NxJsonProjectConfiguration,
77
ProjectConfiguration,
88
ProjectType,
9+
readProjectConfiguration,
910
readWorkspaceConfiguration,
1011
Tree,
1112
} from '@nrwl/devkit';
@@ -25,12 +26,13 @@ import {
2526
GetBuildExecutorConfiguration,
2627
GetLintExecutorConfiguration,
2728
GetServeExecutorConfig,
28-
GetTestExecutorConfig,
2929
NxDotnetProjectGeneratorSchema,
30+
NxDotnetTestGeneratorSchema,
3031
} from '../../models';
3132
import initSchematic from '../init/generator';
33+
import { GenerateTestProject } from './generate-test-project';
3234

33-
interface NormalizedSchema extends NxDotnetProjectGeneratorSchema {
35+
export interface NormalizedSchema extends NxDotnetProjectGeneratorSchema {
3436
projectName: string;
3537
projectRoot: string;
3638
projectDirectory: string;
@@ -39,13 +41,32 @@ interface NormalizedSchema extends NxDotnetProjectGeneratorSchema {
3941
parsedTags: string[];
4042
className: string;
4143
namespaceName: string;
44+
projectType: ProjectType;
4245
}
4346

44-
function normalizeOptions(
47+
export function normalizeOptions(
4548
host: Tree,
46-
options: NxDotnetProjectGeneratorSchema,
47-
projectType: ProjectType,
49+
options: NxDotnetProjectGeneratorSchema | NxDotnetTestGeneratorSchema,
50+
projectType?: ProjectType,
4851
): NormalizedSchema {
52+
if (!('name' in options)) {
53+
// Reconstruct the original parameters as if the test project were generated at the same time as the target project.
54+
const project = readProjectConfiguration(host, options.project);
55+
const projectPaths = project.root.split('/');
56+
const directory = projectPaths.slice(1, -1).join('/'); // The middle portions contain the original path.
57+
const [name] = projectPaths.slice(-1); // The final folder contains the original name.
58+
59+
options = {
60+
name,
61+
language: options.language,
62+
skipOutputPathManipulation: options.skipOutputPathManipulation,
63+
testTemplate: options.testTemplate,
64+
directory,
65+
tags: project.tags?.join(','),
66+
} as NxDotnetProjectGeneratorSchema;
67+
projectType = project.projectType;
68+
}
69+
4970
const name = names(options.name).fileName;
5071
const className = names(options.name).className;
5172
const projectDirectory = options.directory
@@ -79,61 +100,11 @@ function normalizeOptions(
79100
projectLanguage: options.language,
80101
projectTemplate: options.template,
81102
namespaceName,
103+
projectType: projectType ?? 'library',
82104
};
83105
}
84106

85-
async function GenerateTestProject(
86-
schema: NormalizedSchema,
87-
host: Tree,
88-
dotnetClient: DotNetClient,
89-
projectType: ProjectType,
90-
) {
91-
const testRoot = schema.projectRoot + '-test';
92-
const testProjectName = schema.projectName + '-test';
93-
94-
addProjectConfiguration(host, testProjectName, {
95-
root: testRoot,
96-
projectType: projectType,
97-
sourceRoot: `${testRoot}`,
98-
targets: {
99-
build: GetBuildExecutorConfiguration(testRoot),
100-
test: GetTestExecutorConfig(),
101-
lint: GetLintExecutorConfiguration(),
102-
},
103-
tags: schema.parsedTags,
104-
});
105-
106-
const newParams: dotnetNewOptions = [
107-
{
108-
flag: 'language',
109-
value: schema.language,
110-
},
111-
{
112-
flag: 'name',
113-
value: schema.namespaceName + '.Test',
114-
},
115-
{
116-
flag: 'output',
117-
value: schema.projectRoot + '-test',
118-
},
119-
];
120-
121-
if (isDryRun()) {
122-
addDryRunParameter(newParams);
123-
}
124-
125-
dotnetClient.new(schema.testTemplate, newParams);
126-
127-
if (!isDryRun() && !schema.skipOutputPathManipulation) {
128-
const testCsProj = await findProjectFileInPath(testRoot);
129-
SetOutputPath(host, testRoot, testCsProj);
130-
const baseCsProj = await findProjectFileInPath(schema.projectRoot);
131-
SetOutputPath(host, schema.projectRoot, baseCsProj);
132-
dotnetClient.addProjectReference(testCsProj, baseCsProj);
133-
}
134-
}
135-
136-
function SetOutputPath(
107+
export function SetOutputPath(
137108
host: Tree,
138109
projectRootPath: string,
139110
projectFilePath: string,
@@ -227,12 +198,7 @@ export async function GenerateProject(
227198
dotnetClient.new(normalizedOptions.template, newParams);
228199

229200
if (options['testTemplate'] !== 'none') {
230-
await GenerateTestProject(
231-
normalizedOptions,
232-
host,
233-
dotnetClient,
234-
projectType,
235-
);
201+
await GenerateTestProject(host, normalizedOptions, dotnetClient);
236202
} else if (!options.skipOutputPathManipulation) {
237203
SetOutputPath(
238204
host,
@@ -244,7 +210,7 @@ export async function GenerateProject(
244210
await formatFiles(host);
245211
}
246212

247-
function addDryRunParameter(parameters: dotnetNewOptions): void {
213+
export function addDryRunParameter(parameters: dotnetNewOptions): void {
248214
parameters.push({
249215
flag: 'dryRun',
250216
value: true,

0 commit comments

Comments
 (0)