Skip to content

Commit a87fb54

Browse files
committed
Simplify monorepo-workflow-utils tests
The tests for `monorepo-workflow-utils.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 can also simplify how `planRelease` (one of the things we moved out) works so that instead of checking whether the version-to-be-released for a package is the same as the package's current version there, we can check for that in `validateReleaseSpecification`. * 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 in one's head, so the flattened layout here makes it a little more palatable. * Finally, we 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.
1 parent e9b9d06 commit a87fb54

19 files changed

+1711
-1887
lines changed

src/git-utils.test.ts

+31
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as miscUtils from './misc-utils';
33
import {
44
getStdoutFromGitCommandWithin,
55
getRepositoryHttpsUrl,
6+
captureChangesInReleaseBranch,
67
} from './git-utils';
78

89
jest.mock('./misc-utils');
@@ -79,4 +80,34 @@ describe('git-utils', () => {
7980
);
8081
});
8182
});
83+
84+
describe('captureChangesInReleaseBranch', () => {
85+
it('checks out a new branch, stages all files, and creates a new commit', async () => {
86+
const getStdoutFromCommandSpy = jest.spyOn(
87+
miscUtils,
88+
'getStdoutFromCommand',
89+
);
90+
91+
await captureChangesInReleaseBranch(
92+
'/path/to/project',
93+
'some-release-name',
94+
);
95+
96+
expect(getStdoutFromCommandSpy).toHaveBeenCalledWith(
97+
'git',
98+
['checkout', '-b', 'release/some-release-name'],
99+
{ cwd: '/path/to/project' },
100+
);
101+
expect(getStdoutFromCommandSpy).toHaveBeenCalledWith(
102+
'git',
103+
['add', '-A'],
104+
{ cwd: '/path/to/project' },
105+
);
106+
expect(getStdoutFromCommandSpy).toHaveBeenCalledWith(
107+
'git',
108+
['commit', '-m', 'Release some-release-name'],
109+
{ cwd: '/path/to/project' },
110+
);
111+
});
112+
});
82113
});

src/git-utils.ts

+34
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,37 @@ export async function getRepositoryHttpsUrl(
7878

7979
throw new Error(`Unrecognized URL for git remote "origin": ${gitConfigUrl}`);
8080
}
81+
82+
/**
83+
* This function does three things:
84+
*
85+
* 1. Stages all of the changes which have been made to the repo thus far and
86+
* creates a new Git commit which carries the name of the new release.
87+
* 2. Creates a new branch pointed to that commit (which also carries the name
88+
* of the new release).
89+
* 3. Switches to that branch.
90+
*
91+
* @param projectRepositoryPath - The path to the project directory.
92+
* @param releaseName - The name of the release, which will be used to name the
93+
* commit and the branch.
94+
*/
95+
export async function captureChangesInReleaseBranch(
96+
projectRepositoryPath: string,
97+
releaseName: string,
98+
) {
99+
// TODO: What if the index was dirty before this script was run? Or what if
100+
// you're in the middle of a rebase? Might want to check that up front before
101+
// changes are even made.
102+
// TODO: What if this branch already exists? Append the build number?
103+
await getStdoutFromGitCommandWithin(projectRepositoryPath, [
104+
'checkout',
105+
'-b',
106+
`release/${releaseName}`,
107+
]);
108+
await getStdoutFromGitCommandWithin(projectRepositoryPath, ['add', '-A']);
109+
await getStdoutFromGitCommandWithin(projectRepositoryPath, [
110+
'commit',
111+
'-m',
112+
`Release ${releaseName}`,
113+
]);
114+
}

src/initialization-utils.test.ts

+93-13
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,24 @@ import path from 'path';
33
import { when } from 'jest-when';
44
import { buildMockProject, buildMockPackage } from '../tests/unit/helpers';
55
import { initialize } from './initialization-utils';
6+
import * as envUtils from './env-utils';
67
import * as inputsUtils from './inputs-utils';
78
import * as projectUtils from './project-utils';
89

10+
jest.mock('./env-utils');
911
jest.mock('./inputs-utils');
1012
jest.mock('./project-utils');
1113

1214
describe('initialize', () => {
13-
it('returns an object that contains data necessary to run the workflow', async () => {
15+
beforeEach(() => {
16+
jest.useFakeTimers();
17+
});
18+
19+
afterEach(() => {
20+
jest.useRealTimers();
21+
});
22+
23+
it('returns an object derived from command-line arguments and environment variables that contains data necessary to run the workflow', async () => {
1424
const project = buildMockProject();
1525
when(jest.spyOn(inputsUtils, 'readInputs'))
1626
.calledWith(['arg1', 'arg2'])
@@ -19,20 +29,47 @@ describe('initialize', () => {
1929
tempDirectory: '/path/to/temp',
2030
reset: true,
2131
});
32+
jest
33+
.spyOn(envUtils, 'getEnvironmentVariables')
34+
.mockReturnValue({ TODAY: '2022-06-22', EDITOR: undefined });
2235
when(jest.spyOn(projectUtils, 'readProject'))
2336
.calledWith('/path/to/project')
2437
.mockResolvedValue(project);
2538

26-
const config = await initialize(['arg1', 'arg2'], '/path/to/somewhere');
39+
const config = await initialize(['arg1', 'arg2'], '/path/to/cwd');
2740

2841
expect(config).toStrictEqual({
2942
project,
3043
tempDirectoryPath: '/path/to/temp',
3144
reset: true,
45+
today: new Date('2022-06-22'),
3246
});
3347
});
3448

35-
it('uses a default temporary directory based on the name of the package if no such directory was passed as an input', async () => {
49+
it('resolves the project directory relative to the current working directory', async () => {
50+
const project = buildMockProject({
51+
rootPackage: buildMockPackage('@foo/bar'),
52+
});
53+
when(jest.spyOn(inputsUtils, 'readInputs'))
54+
.calledWith(['arg1', 'arg2'])
55+
.mockResolvedValue({
56+
projectDirectory: 'project',
57+
tempDirectory: undefined,
58+
reset: true,
59+
});
60+
jest
61+
.spyOn(envUtils, 'getEnvironmentVariables')
62+
.mockReturnValue({ TODAY: undefined, EDITOR: undefined });
63+
const readProjectSpy = jest
64+
.spyOn(projectUtils, 'readProject')
65+
.mockResolvedValue(project);
66+
67+
await initialize(['arg1', 'arg2'], '/path/to/cwd');
68+
69+
expect(readProjectSpy).toHaveBeenCalledWith('/path/to/cwd/project');
70+
});
71+
72+
it('uses a default temporary directory based on the name of the package', async () => {
3673
const project = buildMockProject({
3774
rootPackage: buildMockPackage('@foo/bar'),
3875
});
@@ -43,20 +80,63 @@ describe('initialize', () => {
4380
tempDirectory: undefined,
4481
reset: true,
4582
});
83+
jest
84+
.spyOn(envUtils, 'getEnvironmentVariables')
85+
.mockReturnValue({ TODAY: undefined, EDITOR: undefined });
4686
when(jest.spyOn(projectUtils, 'readProject'))
4787
.calledWith('/path/to/project')
4888
.mockResolvedValue(project);
4989

50-
const config = await initialize(['arg1', 'arg2'], '/path/to/somewhere');
90+
const config = await initialize(['arg1', 'arg2'], '/path/to/cwd');
5191

52-
expect(config).toStrictEqual({
53-
project,
54-
tempDirectoryPath: path.join(
55-
os.tmpdir(),
56-
'create-release-branch',
57-
'@foo__bar',
58-
),
59-
reset: true,
60-
});
92+
expect(config.tempDirectoryPath).toStrictEqual(
93+
path.join(os.tmpdir(), 'create-release-branch', '@foo__bar'),
94+
);
95+
});
96+
97+
it('uses the current date if TODAY is undefined', async () => {
98+
const project = buildMockProject();
99+
const today = new Date('2022-01-01');
100+
when(jest.spyOn(inputsUtils, 'readInputs'))
101+
.calledWith(['arg1', 'arg2'])
102+
.mockResolvedValue({
103+
projectDirectory: '/path/to/project',
104+
tempDirectory: undefined,
105+
reset: true,
106+
});
107+
jest
108+
.spyOn(envUtils, 'getEnvironmentVariables')
109+
.mockReturnValue({ TODAY: undefined, EDITOR: undefined });
110+
when(jest.spyOn(projectUtils, 'readProject'))
111+
.calledWith('/path/to/project')
112+
.mockResolvedValue(project);
113+
jest.setSystemTime(today);
114+
115+
const config = await initialize(['arg1', 'arg2'], '/path/to/cwd');
116+
117+
expect(config.today).toStrictEqual(today);
118+
});
119+
120+
it('uses the current date if TODAY is not a parsable date', async () => {
121+
const project = buildMockProject();
122+
const today = new Date('2022-01-01');
123+
when(jest.spyOn(inputsUtils, 'readInputs'))
124+
.calledWith(['arg1', 'arg2'])
125+
.mockResolvedValue({
126+
projectDirectory: '/path/to/project',
127+
tempDirectory: undefined,
128+
reset: true,
129+
});
130+
jest
131+
.spyOn(envUtils, 'getEnvironmentVariables')
132+
.mockReturnValue({ TODAY: 'asdfgdasf', EDITOR: undefined });
133+
when(jest.spyOn(projectUtils, 'readProject'))
134+
.calledWith('/path/to/project')
135+
.mockResolvedValue(project);
136+
jest.setSystemTime(today);
137+
138+
const config = await initialize(['arg1', 'arg2'], '/path/to/cwd');
139+
140+
expect(config.today).toStrictEqual(today);
61141
});
62142
});

src/initialization-utils.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os from 'os';
22
import path from 'path';
3+
import { getEnvironmentVariables } from './env-utils';
34
import { readProject, Project } from './project-utils';
45
import { readInputs } from './inputs-utils';
56

@@ -14,8 +15,15 @@ import { readInputs } from './inputs-utils';
1415
export async function initialize(
1516
argv: string[],
1617
cwd: string,
17-
): Promise<{ project: Project; tempDirectoryPath: string; reset: boolean }> {
18+
): Promise<{
19+
project: Project;
20+
tempDirectoryPath: string;
21+
reset: boolean;
22+
today: Date;
23+
}> {
1824
const inputs = await readInputs(argv);
25+
const { TODAY } = getEnvironmentVariables();
26+
1927
const projectDirectoryPath = path.resolve(cwd, inputs.projectDirectory);
2028
const project = await readProject(projectDirectoryPath);
2129
const tempDirectoryPath =
@@ -26,6 +34,11 @@ export async function initialize(
2634
project.rootPackage.validatedManifest.name.replace('/', '__'),
2735
)
2836
: path.resolve(cwd, inputs.tempDirectory);
37+
const parsedTodayTimestamp =
38+
TODAY === undefined ? NaN : new Date(TODAY).getTime();
39+
const today = isNaN(parsedTodayTimestamp)
40+
? new Date()
41+
: new Date(parsedTodayTimestamp);
2942

30-
return { project, tempDirectoryPath, reset: inputs.reset };
43+
return { project, tempDirectoryPath, reset: inputs.reset, today };
3144
}

src/main.test.ts

+5
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ jest.mock('./monorepo-workflow-utils');
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.spyOn(initializationUtils, 'initialize').mockResolvedValue({
1617
project,
1718
tempDirectoryPath: '/path/to/temp/directory',
1819
reset: false,
20+
today,
1921
});
2022
const followMonorepoWorkflowSpy = jest
2123
.spyOn(monorepoWorkflowUtils, 'followMonorepoWorkflow')
@@ -32,19 +34,22 @@ describe('main', () => {
3234
project,
3335
tempDirectoryPath: '/path/to/temp/directory',
3436
firstRemovingExistingReleaseSpecification: false,
37+
today,
3538
stdout,
3639
stderr,
3740
});
3841
});
3942

4043
it('executes the polyrepo workflow if the project is within a polyrepo', async () => {
4144
const project = buildMockProject({ isMonorepo: false });
45+
const today = new Date();
4246
const stdout = fs.createWriteStream('/dev/null');
4347
const stderr = fs.createWriteStream('/dev/null');
4448
jest.spyOn(initializationUtils, 'initialize').mockResolvedValue({
4549
project,
4650
tempDirectoryPath: '/path/to/temp/directory',
4751
reset: false,
52+
today,
4853
});
4954
const followMonorepoWorkflowSpy = jest
5055
.spyOn(monorepoWorkflowUtils, 'followMonorepoWorkflow')

src/main.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ export async function main({
2323
stdout: Pick<WriteStream, 'write'>;
2424
stderr: Pick<WriteStream, 'write'>;
2525
}) {
26-
const { project, tempDirectoryPath, reset } = await initialize(argv, cwd);
26+
const { project, tempDirectoryPath, reset, today } = await initialize(
27+
argv,
28+
cwd,
29+
);
2730

2831
if (project.isMonorepo) {
2932
stdout.write(
@@ -33,6 +36,7 @@ export async function main({
3336
project,
3437
tempDirectoryPath,
3538
firstRemovingExistingReleaseSpecification: reset,
39+
today,
3640
stdout,
3741
stderr,
3842
});

0 commit comments

Comments
 (0)