Skip to content

Commit

Permalink
Simplify monorepo-workflow-utils tests
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mcmire committed Aug 22, 2022
1 parent 372229e commit be53bb8
Show file tree
Hide file tree
Showing 19 changed files with 1,785 additions and 2,003 deletions.
134 changes: 122 additions & 12 deletions src/initial-parameters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,24 @@ import { when } from 'jest-when';
import { buildMockProject, buildMockPackage } from '../tests/unit/helpers';
import { determineInitialParameters } from './initial-parameters';
import * as commandLineArgumentsModule from './command-line-arguments';
import * as envModule from './env';
import * as projectModule from './project';

jest.mock('./command-line-arguments');
jest.mock('./env');
jest.mock('./project');

describe('initial-parameters', () => {
describe('determineInitialParameters', () => {
it('returns an object that contains data necessary to run the workflow', async () => {
beforeEach(() => {
jest.useFakeTimers();
});

afterEach(() => {
jest.useRealTimers();
});

it('returns an object derived from command-line arguments and environment variables that contains data necessary to run the workflow', async () => {
const project = buildMockProject();
when(jest.spyOn(commandLineArgumentsModule, 'readCommandLineArguments'))
.calledWith(['arg1', 'arg2'])
Expand All @@ -20,6 +30,9 @@ describe('initial-parameters', () => {
tempDirectory: '/path/to/temp',
reset: true,
});
jest
.spyOn(envModule, 'getEnvironmentVariables')
.mockReturnValue({ TODAY: '2022-06-22', EDITOR: undefined });
when(jest.spyOn(projectModule, 'readProject'))
.calledWith('/path/to/project')
.mockResolvedValue(project);
Expand All @@ -33,10 +46,58 @@ describe('initial-parameters', () => {
project,
tempDirectoryPath: '/path/to/temp',
reset: true,
today: new Date('2022-06-22'),
});
});

it('resolves the given project directory relative to the current working directory', async () => {
const project = buildMockProject({
rootPackage: buildMockPackage(),
});
when(jest.spyOn(commandLineArgumentsModule, 'readCommandLineArguments'))
.calledWith(['arg1', 'arg2'])
.mockResolvedValue({
projectDirectory: 'project',
tempDirectory: undefined,
reset: true,
});
jest
.spyOn(envModule, 'getEnvironmentVariables')
.mockReturnValue({ TODAY: undefined, EDITOR: undefined });
const readProjectSpy = jest
.spyOn(projectModule, 'readProject')
.mockResolvedValue(project);

await determineInitialParameters(['arg1', 'arg2'], '/path/to/cwd');

expect(readProjectSpy).toHaveBeenCalledWith('/path/to/cwd/project');
});

it('resolves the given temporary directory relative to the current working directory', async () => {
const project = buildMockProject();
when(jest.spyOn(commandLineArgumentsModule, 'readCommandLineArguments'))
.calledWith(['arg1', 'arg2'])
.mockResolvedValue({
projectDirectory: '/path/to/project',
tempDirectory: 'tmp',
reset: true,
});
jest
.spyOn(envModule, 'getEnvironmentVariables')
.mockReturnValue({ TODAY: undefined, EDITOR: undefined });
when(jest.spyOn(projectModule, 'readProject'))
.calledWith('/path/to/project')
.mockResolvedValue(project);

const config = await determineInitialParameters(
['arg1', 'arg2'],
'/path/to/cwd',
);

expect(config.tempDirectoryPath).toStrictEqual('/path/to/cwd/tmp');
});

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

const config = await determineInitialParameters(
['arg1', 'arg2'],
'/path/to/somewhere',
'/path/to/cwd',
);

expect(config).toStrictEqual({
project,
tempDirectoryPath: path.join(
os.tmpdir(),
'create-release-branch',
'@foo__bar',
),
reset: true,
});
expect(config.tempDirectoryPath).toStrictEqual(
path.join(os.tmpdir(), 'create-release-branch', '@foo__bar'),
);
});

it('uses the current date if the TODAY environment variable was not provided', async () => {
const project = buildMockProject();
const today = new Date('2022-01-01');
when(jest.spyOn(commandLineArgumentsModule, 'readCommandLineArguments'))
.calledWith(['arg1', 'arg2'])
.mockResolvedValue({
projectDirectory: '/path/to/project',
tempDirectory: undefined,
reset: true,
});
jest
.spyOn(envModule, 'getEnvironmentVariables')
.mockReturnValue({ TODAY: undefined, EDITOR: undefined });
when(jest.spyOn(projectModule, 'readProject'))
.calledWith('/path/to/project')
.mockResolvedValue(project);
jest.setSystemTime(today);

const config = await determineInitialParameters(
['arg1', 'arg2'],
'/path/to/cwd',
);

expect(config.today).toStrictEqual(today);
});

it('uses the current date if TODAY is not a parsable date', async () => {
const project = buildMockProject();
const today = new Date('2022-01-01');
when(jest.spyOn(commandLineArgumentsModule, 'readCommandLineArguments'))
.calledWith(['arg1', 'arg2'])
.mockResolvedValue({
projectDirectory: '/path/to/project',
tempDirectory: undefined,
reset: true,
});
jest
.spyOn(envModule, 'getEnvironmentVariables')
.mockReturnValue({ TODAY: 'asdfgdasf', EDITOR: undefined });
when(jest.spyOn(projectModule, 'readProject'))
.calledWith('/path/to/project')
.mockResolvedValue(project);
jest.setSystemTime(today);

const config = await determineInitialParameters(
['arg1', 'arg2'],
'/path/to/cwd',
);

expect(config.today).toStrictEqual(today);
});
});
});
11 changes: 10 additions & 1 deletion src/initial-parameters.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import os from 'os';
import path from 'path';
import { getEnvironmentVariables } from './env';
import { readCommandLineArguments } from './command-line-arguments';
import { readProject, Project } from './project';

interface InitialParameters {
project: Project;
tempDirectoryPath: string;
reset: boolean;
today: Date;
}

/**
Expand All @@ -22,6 +24,8 @@ export async function determineInitialParameters(
cwd: string,
): Promise<InitialParameters> {
const inputs = await readCommandLineArguments(argv);
const { TODAY } = getEnvironmentVariables();

const projectDirectoryPath = path.resolve(cwd, inputs.projectDirectory);
const project = await readProject(projectDirectoryPath);
const tempDirectoryPath =
Expand All @@ -32,6 +36,11 @@ export async function determineInitialParameters(
project.rootPackage.validatedManifest.name.replace('/', '__'),
)
: path.resolve(cwd, inputs.tempDirectory);
const parsedTodayTimestamp =
TODAY === undefined ? NaN : new Date(TODAY).getTime();
const today = isNaN(parsedTodayTimestamp)
? new Date()
: new Date(parsedTodayTimestamp);

return { project, tempDirectoryPath, reset: inputs.reset };
return { project, tempDirectoryPath, reset: inputs.reset, today };
}
5 changes: 5 additions & 0 deletions src/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ jest.mock('./monorepo-workflow-operations');
describe('main', () => {
it('executes the monorepo workflow if the project is a monorepo', async () => {
const project = buildMockProject({ isMonorepo: true });
const today = new Date();
const stdout = fs.createWriteStream('/dev/null');
const stderr = fs.createWriteStream('/dev/null');
jest
Expand All @@ -18,6 +19,7 @@ describe('main', () => {
project,
tempDirectoryPath: '/path/to/temp/directory',
reset: false,
today,
});
const followMonorepoWorkflowSpy = jest
.spyOn(monorepoWorkflowOperations, 'followMonorepoWorkflow')
Expand All @@ -34,13 +36,15 @@ describe('main', () => {
project,
tempDirectoryPath: '/path/to/temp/directory',
firstRemovingExistingReleaseSpecification: false,
today,
stdout,
stderr,
});
});

it('executes the polyrepo workflow if the project is within a polyrepo', async () => {
const project = buildMockProject({ isMonorepo: false });
const today = new Date();
const stdout = fs.createWriteStream('/dev/null');
const stderr = fs.createWriteStream('/dev/null');
jest
Expand All @@ -49,6 +53,7 @@ describe('main', () => {
project,
tempDirectoryPath: '/path/to/temp/directory',
reset: false,
today,
});
const followMonorepoWorkflowSpy = jest
.spyOn(monorepoWorkflowOperations, 'followMonorepoWorkflow')
Expand Down
3 changes: 2 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export async function main({
stdout: Pick<WriteStream, 'write'>;
stderr: Pick<WriteStream, 'write'>;
}) {
const { project, tempDirectoryPath, reset } =
const { project, tempDirectoryPath, reset, today } =
await determineInitialParameters(argv, cwd);

if (project.isMonorepo) {
Expand All @@ -36,6 +36,7 @@ export async function main({
project,
tempDirectoryPath,
firstRemovingExistingReleaseSpecification: reset,
today,
stdout,
stderr,
});
Expand Down
Loading

0 comments on commit be53bb8

Please sign in to comment.