Skip to content

Commit 3db4897

Browse files
committed
Simplify monorepo-workflow-operations
The tests for `monorepo-workflow-operations.ts` are not as readable or maintainable as I'd like them to be. This commit attempts to fix that. * While there isn't a whole lot we can do in terms of the scenarios we're testing because the function makes a lot of calls and the logic inside of it is somewhat complicated, we can at least move code that is responsible for parsing the TODAY environment variable, building a ReleasePlan object from the release spec, and applying the updates to the repos themselves out into separate files. This simplifies the test data and removes a few tests entirely. * We also simplify the structure of the tests so that we aren't using so many nested `describe` blocks. This ends up being very difficult to keep straight, so the flattened layout here makes it a little more palatable. * We also simplify the setup code for each test. Currently we are mocking all of the dependencies for `followMonorepoWorkflow` in one go, but we're doing so in a way that forces the reader to wade through a bunch of type definitions. That isn't really that helpful. The most complicated part of reading the tests for `followMonorepoWorkflow` isn't the dependencies — it's the logic. So we take all of the decision points we have to make in the implementation and represent those as options to our setup function in the tests so it's as clear as possible which exact scenario is being tested just by reading the test. * Finally while we're moving things around, we can also move the check we perform to ensure that the new version of a package is greater than its existing version so that it is earlier in the entire workflow, i.e. from building the release plan to the validating the release specification. This also reduces the number of tests we have to write.
1 parent 372229e commit 3db4897

19 files changed

+1785
-2003
lines changed

src/initial-parameters.test.ts

+122-12
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,24 @@ import { when } from 'jest-when';
44
import { buildMockProject, buildMockPackage } from '../tests/unit/helpers';
55
import { determineInitialParameters } from './initial-parameters';
66
import * as commandLineArgumentsModule from './command-line-arguments';
7+
import * as envModule from './env';
78
import * as projectModule from './project';
89

910
jest.mock('./command-line-arguments');
11+
jest.mock('./env');
1012
jest.mock('./project');
1113

1214
describe('initial-parameters', () => {
1315
describe('determineInitialParameters', () => {
14-
it('returns an object that contains data necessary to run the workflow', async () => {
16+
beforeEach(() => {
17+
jest.useFakeTimers();
18+
});
19+
20+
afterEach(() => {
21+
jest.useRealTimers();
22+
});
23+
24+
it('returns an object derived from command-line arguments and environment variables that contains data necessary to run the workflow', async () => {
1525
const project = buildMockProject();
1626
when(jest.spyOn(commandLineArgumentsModule, 'readCommandLineArguments'))
1727
.calledWith(['arg1', 'arg2'])
@@ -20,6 +30,9 @@ describe('initial-parameters', () => {
2030
tempDirectory: '/path/to/temp',
2131
reset: true,
2232
});
33+
jest
34+
.spyOn(envModule, 'getEnvironmentVariables')
35+
.mockReturnValue({ TODAY: '2022-06-22', EDITOR: undefined });
2336
when(jest.spyOn(projectModule, 'readProject'))
2437
.calledWith('/path/to/project')
2538
.mockResolvedValue(project);
@@ -33,10 +46,58 @@ describe('initial-parameters', () => {
3346
project,
3447
tempDirectoryPath: '/path/to/temp',
3548
reset: true,
49+
today: new Date('2022-06-22'),
50+
});
51+
});
52+
53+
it('resolves the given project directory relative to the current working directory', async () => {
54+
const project = buildMockProject({
55+
rootPackage: buildMockPackage(),
3656
});
57+
when(jest.spyOn(commandLineArgumentsModule, 'readCommandLineArguments'))
58+
.calledWith(['arg1', 'arg2'])
59+
.mockResolvedValue({
60+
projectDirectory: 'project',
61+
tempDirectory: undefined,
62+
reset: true,
63+
});
64+
jest
65+
.spyOn(envModule, 'getEnvironmentVariables')
66+
.mockReturnValue({ TODAY: undefined, EDITOR: undefined });
67+
const readProjectSpy = jest
68+
.spyOn(projectModule, 'readProject')
69+
.mockResolvedValue(project);
70+
71+
await determineInitialParameters(['arg1', 'arg2'], '/path/to/cwd');
72+
73+
expect(readProjectSpy).toHaveBeenCalledWith('/path/to/cwd/project');
74+
});
75+
76+
it('resolves the given temporary directory relative to the current working directory', async () => {
77+
const project = buildMockProject();
78+
when(jest.spyOn(commandLineArgumentsModule, 'readCommandLineArguments'))
79+
.calledWith(['arg1', 'arg2'])
80+
.mockResolvedValue({
81+
projectDirectory: '/path/to/project',
82+
tempDirectory: 'tmp',
83+
reset: true,
84+
});
85+
jest
86+
.spyOn(envModule, 'getEnvironmentVariables')
87+
.mockReturnValue({ TODAY: undefined, EDITOR: undefined });
88+
when(jest.spyOn(projectModule, 'readProject'))
89+
.calledWith('/path/to/project')
90+
.mockResolvedValue(project);
91+
92+
const config = await determineInitialParameters(
93+
['arg1', 'arg2'],
94+
'/path/to/cwd',
95+
);
96+
97+
expect(config.tempDirectoryPath).toStrictEqual('/path/to/cwd/tmp');
3798
});
3899

39-
it('uses a default temporary directory based on the name of the package if no such directory was passed as an input', async () => {
100+
it('uses a default temporary directory based on the name of the package if no temporary directory was given', async () => {
40101
const project = buildMockProject({
41102
rootPackage: buildMockPackage('@foo/bar'),
42103
});
@@ -47,24 +108,73 @@ describe('initial-parameters', () => {
47108
tempDirectory: undefined,
48109
reset: true,
49110
});
111+
jest
112+
.spyOn(envModule, 'getEnvironmentVariables')
113+
.mockReturnValue({ TODAY: undefined, EDITOR: undefined });
50114
when(jest.spyOn(projectModule, 'readProject'))
51115
.calledWith('/path/to/project')
52116
.mockResolvedValue(project);
53117

54118
const config = await determineInitialParameters(
55119
['arg1', 'arg2'],
56-
'/path/to/somewhere',
120+
'/path/to/cwd',
57121
);
58122

59-
expect(config).toStrictEqual({
60-
project,
61-
tempDirectoryPath: path.join(
62-
os.tmpdir(),
63-
'create-release-branch',
64-
'@foo__bar',
65-
),
66-
reset: true,
67-
});
123+
expect(config.tempDirectoryPath).toStrictEqual(
124+
path.join(os.tmpdir(), 'create-release-branch', '@foo__bar'),
125+
);
126+
});
127+
128+
it('uses the current date if the TODAY environment variable was not provided', async () => {
129+
const project = buildMockProject();
130+
const today = new Date('2022-01-01');
131+
when(jest.spyOn(commandLineArgumentsModule, 'readCommandLineArguments'))
132+
.calledWith(['arg1', 'arg2'])
133+
.mockResolvedValue({
134+
projectDirectory: '/path/to/project',
135+
tempDirectory: undefined,
136+
reset: true,
137+
});
138+
jest
139+
.spyOn(envModule, 'getEnvironmentVariables')
140+
.mockReturnValue({ TODAY: undefined, EDITOR: undefined });
141+
when(jest.spyOn(projectModule, 'readProject'))
142+
.calledWith('/path/to/project')
143+
.mockResolvedValue(project);
144+
jest.setSystemTime(today);
145+
146+
const config = await determineInitialParameters(
147+
['arg1', 'arg2'],
148+
'/path/to/cwd',
149+
);
150+
151+
expect(config.today).toStrictEqual(today);
152+
});
153+
154+
it('uses the current date if TODAY is not a parsable date', async () => {
155+
const project = buildMockProject();
156+
const today = new Date('2022-01-01');
157+
when(jest.spyOn(commandLineArgumentsModule, 'readCommandLineArguments'))
158+
.calledWith(['arg1', 'arg2'])
159+
.mockResolvedValue({
160+
projectDirectory: '/path/to/project',
161+
tempDirectory: undefined,
162+
reset: true,
163+
});
164+
jest
165+
.spyOn(envModule, 'getEnvironmentVariables')
166+
.mockReturnValue({ TODAY: 'asdfgdasf', EDITOR: undefined });
167+
when(jest.spyOn(projectModule, 'readProject'))
168+
.calledWith('/path/to/project')
169+
.mockResolvedValue(project);
170+
jest.setSystemTime(today);
171+
172+
const config = await determineInitialParameters(
173+
['arg1', 'arg2'],
174+
'/path/to/cwd',
175+
);
176+
177+
expect(config.today).toStrictEqual(today);
68178
});
69179
});
70180
});

src/initial-parameters.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import os from 'os';
22
import path from 'path';
3+
import { getEnvironmentVariables } from './env';
34
import { readCommandLineArguments } from './command-line-arguments';
45
import { readProject, Project } from './project';
56

67
interface InitialParameters {
78
project: Project;
89
tempDirectoryPath: string;
910
reset: boolean;
11+
today: Date;
1012
}
1113

1214
/**
@@ -22,6 +24,8 @@ export async function determineInitialParameters(
2224
cwd: string,
2325
): Promise<InitialParameters> {
2426
const inputs = await readCommandLineArguments(argv);
27+
const { TODAY } = getEnvironmentVariables();
28+
2529
const projectDirectoryPath = path.resolve(cwd, inputs.projectDirectory);
2630
const project = await readProject(projectDirectoryPath);
2731
const tempDirectoryPath =
@@ -32,6 +36,11 @@ export async function determineInitialParameters(
3236
project.rootPackage.validatedManifest.name.replace('/', '__'),
3337
)
3438
: path.resolve(cwd, inputs.tempDirectory);
39+
const parsedTodayTimestamp =
40+
TODAY === undefined ? NaN : new Date(TODAY).getTime();
41+
const today = isNaN(parsedTodayTimestamp)
42+
? new Date()
43+
: new Date(parsedTodayTimestamp);
3544

36-
return { project, tempDirectoryPath, reset: inputs.reset };
45+
return { project, tempDirectoryPath, reset: inputs.reset, today };
3746
}

src/main.test.ts

+5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ jest.mock('./monorepo-workflow-operations');
1010
describe('main', () => {
1111
it('executes the monorepo workflow if the project is a monorepo', async () => {
1212
const project = buildMockProject({ isMonorepo: true });
13+
const today = new Date();
1314
const stdout = fs.createWriteStream('/dev/null');
1415
const stderr = fs.createWriteStream('/dev/null');
1516
jest
@@ -18,6 +19,7 @@ describe('main', () => {
1819
project,
1920
tempDirectoryPath: '/path/to/temp/directory',
2021
reset: false,
22+
today,
2123
});
2224
const followMonorepoWorkflowSpy = jest
2325
.spyOn(monorepoWorkflowOperations, 'followMonorepoWorkflow')
@@ -34,13 +36,15 @@ describe('main', () => {
3436
project,
3537
tempDirectoryPath: '/path/to/temp/directory',
3638
firstRemovingExistingReleaseSpecification: false,
39+
today,
3740
stdout,
3841
stderr,
3942
});
4043
});
4144

4245
it('executes the polyrepo workflow if the project is within a polyrepo', async () => {
4346
const project = buildMockProject({ isMonorepo: false });
47+
const today = new Date();
4448
const stdout = fs.createWriteStream('/dev/null');
4549
const stderr = fs.createWriteStream('/dev/null');
4650
jest
@@ -49,6 +53,7 @@ describe('main', () => {
4953
project,
5054
tempDirectoryPath: '/path/to/temp/directory',
5155
reset: false,
56+
today,
5257
});
5358
const followMonorepoWorkflowSpy = jest
5459
.spyOn(monorepoWorkflowOperations, 'followMonorepoWorkflow')

src/main.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export async function main({
2525
stdout: Pick<WriteStream, 'write'>;
2626
stderr: Pick<WriteStream, 'write'>;
2727
}) {
28-
const { project, tempDirectoryPath, reset } =
28+
const { project, tempDirectoryPath, reset, today } =
2929
await determineInitialParameters(argv, cwd);
3030

3131
if (project.isMonorepo) {
@@ -36,6 +36,7 @@ export async function main({
3636
project,
3737
tempDirectoryPath,
3838
firstRemovingExistingReleaseSpecification: reset,
39+
today,
3940
stdout,
4041
stderr,
4142
});

0 commit comments

Comments
 (0)