Refactor to enforce strictNullChecks

This commit applies `strictNullChecks` to the entire codebase to improve
maintainability and type safety. Key changes include:

- Remove some explicit null-checks where unnecessary.
- Add necessary null-checks.
- Refactor static factory functions for a more functional approach.
- Improve some test names and contexts for better debugging.
- Add unit tests for any additional logic introduced.
- Refactor `createPositionFromRegexFullMatch` to its own function as the
  logic is reused.
- Prefer `find` prefix on functions that may return `undefined` and
  `get` prefix for those that always return a value.
This commit is contained in:
undergroundwires
2023-11-12 22:54:00 +01:00
parent 7ab16ecccb
commit 949fac1a7c
294 changed files with 2477 additions and 2738 deletions

View File

@@ -183,16 +183,37 @@ describe('CodeRunner', () => {
.filter((command) => expectedOrder.includes(command));
expect(expectedOrder).to.deep.equal(actualOrder);
});
it('throws with unsupported os', async () => {
// arrange
const unknownOs = OperatingSystem.Android;
const expectedError = `unsupported os: ${OperatingSystem[unknownOs]}`;
const context = new TestContext()
.withOs(unknownOs);
// act
const act = async () => { await context.runCode(); };
// assert
expectThrowsAsync(act, expectedError);
describe('throws with invalid OS', () => {
const testScenarios: ReadonlyArray<{
readonly description: string;
readonly invalidOs: OperatingSystem | undefined;
readonly expectedError: string;
}> = [
(() => {
const unsupportedOs = OperatingSystem.Android;
return {
description: 'unsupported OS',
invalidOs: unsupportedOs,
expectedError: `unsupported os: ${OperatingSystem[unsupportedOs]}`,
};
})(),
{
description: 'unknown OS',
invalidOs: undefined,
expectedError: 'Unidentified operating system',
},
];
testScenarios.forEach(({ description, invalidOs, expectedError }) => {
it(description, async () => {
// arrange
const context = new TestContext()
.withOs(invalidOs);
// act
const act = async () => { await context.runCode(); };
// assert
await expectThrowsAsync(act, expectedError);
});
});
});
});
});
@@ -204,7 +225,7 @@ class TestContext {
private fileExtension = 'fileExtension';
private os = OperatingSystem.Windows;
private os: OperatingSystem | undefined = OperatingSystem.Windows;
private systemOperations: ISystemOperations = new SystemOperationsStub();
@@ -229,7 +250,7 @@ class TestContext {
return this.withSystemOperations(stub);
}
public withOs(os: OperatingSystem) {
public withOs(os: OperatingSystem | undefined) {
this.os = os;
return this;
}

View File

@@ -5,6 +5,7 @@ import { itIsSingleton } from '@tests/unit/shared/TestCases/SingletonTests';
import { EnvironmentVariablesFactory, EnvironmentVariablesValidator } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
import { ViteEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/Vite/ViteEnvironmentVariables';
import { IEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/IEnvironmentVariables';
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
describe('EnvironmentVariablesFactory', () => {
describe('instance', () => {
@@ -23,7 +24,7 @@ describe('EnvironmentVariablesFactory', () => {
});
it('validates its instance', () => {
// arrange
let validatedInstance: IEnvironmentVariables;
let validatedInstance: IEnvironmentVariables | undefined;
const validatorMock = (instanceToValidate: IEnvironmentVariables) => {
validatedInstance = instanceToValidate;
};
@@ -31,6 +32,7 @@ describe('EnvironmentVariablesFactory', () => {
const sut = new TestableEnvironmentVariablesFactory(validatorMock);
const actualInstance = sut.instance;
// assert
expectExists(validatedInstance);
expect(actualInstance).to.equal(validatedInstance);
});
it('throws error if validator fails', () => {

View File

@@ -1,5 +1,4 @@
import { describe, it, expect } from 'vitest';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { EnvironmentVariablesStub } from '@tests/unit/shared/Stubs/EnvironmentVariablesStub';
import { validateEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesValidator';
import { IEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/IEnvironmentVariables';
@@ -28,17 +27,6 @@ describe('EnvironmentVariablesValidator', () => {
expect(act).to.not.throw();
});
describe('throws as expected', () => {
describe('"missing environment" if environment is not provided', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing environment';
const environment = absentValue;
// act
const act = () => validateEnvironmentVariables(environment);
// assert
expect(act).to.throw(expectedError);
});
});
it('"missing keys" if environment has properties with missing values', () => {
// arrange
const expectedError = 'Environment keys missing: name, homepageUrl';

View File

@@ -1,7 +1,7 @@
import { describe, it, expect } from 'vitest';
import { EventSubscriptionCollection } from '@/infrastructure/Events/EventSubscriptionCollection';
import { EventSubscriptionStub } from '@tests/unit/shared/Stubs/EventSubscriptionStub';
import { itEachAbsentCollectionValue, itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
describe('EventSubscriptionCollection', () => {
@@ -140,7 +140,7 @@ function describeSubscriptionValidations(
handleValue: (subscriptions: IEventSubscription[]) => void,
) {
describe('throws error if no subscriptions are provided', () => {
itEachAbsentCollectionValue((absentValue) => {
itEachAbsentCollectionValue<IEventSubscription>((absentValue) => {
// arrange
const expectedError = 'missing subscriptions';
@@ -149,24 +149,6 @@ function describeSubscriptionValidations(
// assert
expect(act).to.throw(expectedError);
});
});
describe('throws error if nullish subscriptions are provided', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing subscription in list';
const subscriptions = [
new EventSubscriptionStub(),
absentValue,
new EventSubscriptionStub(),
];
// act
const act = () => handleValue(subscriptions);
// assert
expect(act).to.throw(expectedError);
});
}, { excludeUndefined: true, excludeNull: true });
});
}

View File

@@ -1,7 +1,6 @@
import { describe, it, expect } from 'vitest';
import { NumericEntityStub } from '@tests/unit/shared/Stubs/NumericEntityStub';
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
describe('InMemoryRepository', () => {
describe('exists', () => {
@@ -52,20 +51,6 @@ describe('InMemoryRepository', () => {
expect(actual.length).to.equal(expected.length);
expect(actual.item).to.deep.equal(expected.item);
});
describe('throws when item is absent', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing item';
const sut = new InMemoryRepository<number, NumericEntityStub>();
const item = absentValue;
// act
const act = () => sut.addItem(item);
// assert
expect(act).to.throw(expectedError);
});
});
});
it('removeItem removes', () => {
// arrange
@@ -116,20 +101,6 @@ describe('InMemoryRepository', () => {
const actual = sut.getItems();
expect(actual).to.deep.equal(expected);
});
describe('throws when item is absent', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing item';
const sut = new InMemoryRepository<number, NumericEntityStub>();
const item = absentValue;
// act
const act = () => sut.addOrUpdateItem(item);
// assert
expect(act).to.throw(expectedError);
});
});
});
describe('getById', () => {
it('returns entity if it exists', () => {
@@ -144,13 +115,15 @@ describe('InMemoryRepository', () => {
// assert
expect(actual).to.deep.equal(expected);
});
it('returns undefined if it does not exist', () => {
it('throws if item does not exist', () => {
// arrange
const id = 31;
const expectedError = `missing item: ${id}`;
const sut = new InMemoryRepository<number, NumericEntityStub>([]);
// act
const actual = sut.getById(31);
const act = () => sut.getById(id);
// assert
expect(actual).to.equal(undefined);
expect(act).to.throw(expectedError);
});
});
});

View File

@@ -9,7 +9,7 @@ describe('ConsoleLogger', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing console';
const console = absentValue;
const console = absentValue as never;
// act
const act = () => new ConsoleLogger(console);
// assert
@@ -32,10 +32,25 @@ describe('ConsoleLogger', () => {
expect(consoleMock.callHistory[0].args).to.deep.equal(expectedParams);
});
});
describe('throws if log function is missing', () => {
itEachLoggingMethod((functionName, testParameters) => {
// arrange
const expectedError = `missing "${functionName}" function`;
const consoleMock = {} as Partial<Console>;
consoleMock[functionName] = undefined;
const logger = new ConsoleLogger(consoleMock);
// act
const act = () => logger[functionName](...testParameters);
// assert
expect(act).to.throw(expectedError);
});
});
});
class MockConsole
extends StubWithObservableMethodCalls<Partial<Console>>
extends StubWithObservableMethodCalls<Console>
implements Partial<Console> {
public info(...args: unknown[]) {
this.registerMethodCall({

View File

@@ -10,31 +10,48 @@ describe('ElectronLogger', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing logger';
const electronLog = absentValue;
const electronLog = absentValue as never;
// act
const act = () => createElectronLogger(electronLog);
// assert
expect(act).to.throw(expectedError);
}, { excludeUndefined: true });
});
itEachLoggingMethod((functionName, testParameters) => {
// arrange
const expectedParams = testParameters;
const electronLogMock = new MockElectronLog();
const logger = createElectronLogger(electronLogMock);
describe('throws if log function is missing', () => {
itEachLoggingMethod((functionName, testParameters) => {
// arrange
const expectedError = `missing "${functionName}" function`;
const electronLogMock = {} as Partial<ElectronLog>;
electronLogMock[functionName] = undefined;
const logger = createElectronLogger(electronLogMock);
// act
logger[functionName](...expectedParams);
// act
const act = () => logger[functionName](...testParameters);
// assert
expect(electronLogMock.callHistory).to.have.lengthOf(1);
expect(electronLogMock.callHistory[0].methodName).to.equal(functionName);
expect(electronLogMock.callHistory[0].args).to.deep.equal(expectedParams);
// assert
expect(act).to.throw(expectedError);
});
});
describe('methods log the provided params', () => {
itEachLoggingMethod((functionName, testParameters) => {
// arrange
const expectedParams = testParameters;
const electronLogMock = new MockElectronLog();
const logger = createElectronLogger(electronLogMock);
// act
logger[functionName](...expectedParams);
// assert
expect(electronLogMock.callHistory).to.have.lengthOf(1);
expect(electronLogMock.callHistory[0].methodName).to.equal(functionName);
expect(electronLogMock.callHistory[0].args).to.deep.equal(expectedParams);
});
});
});
class MockElectronLog
extends StubWithObservableMethodCalls<Partial<ElectronLog>>
extends StubWithObservableMethodCalls<ElectronLog>
implements Partial<ElectronLog> {
public info(...args: unknown[]) {
this.registerMethodCall({

View File

@@ -2,13 +2,15 @@ import { it } from 'vitest';
import { FunctionKeys } from '@/TypeHelpers';
import { ILogger } from '@/infrastructure/Log/ILogger';
type TestParameters = [string, number, { some: string }];
export function itEachLoggingMethod(
handler: (
functionName: keyof ILogger,
testParameters?: unknown[]
testParameters: TestParameters,
) => void,
) {
const testParameters = ['test', 123, { some: 'object' }];
const testParameters: TestParameters = ['test', 123, { some: 'object' }];
const loggerMethods: Array<FunctionKeys<ILogger>> = [
'info',
];

View File

@@ -11,7 +11,7 @@ describe('WindowInjectedLogger', () => {
// arrange
const expectedError = 'missing log';
const windowVariables = new WindowVariablesStub()
.withLog(absentValue);
.withLog(absentValue as never);
// act
const act = () => new WindowInjectedLogger(windowVariables);
// assert

View File

@@ -15,7 +15,7 @@ describe('BrowserOsDetector', () => {
const actual = sut.detect(userAgent);
// assert
expect(actual).to.equal(expected);
});
}, { excludeNull: true, excludeUndefined: true });
});
it('detects as expected', () => {
BrowserOsTestCases.forEach((testCase) => {
@@ -27,7 +27,7 @@ describe('BrowserOsDetector', () => {
expect(actual).to.equal(testCase.expectedOs, printMessage());
function printMessage(): string {
return `Expected: "${OperatingSystem[testCase.expectedOs]}"\n`
+ `Actual: "${OperatingSystem[actual]}"\n`
+ `Actual: "${actual === undefined ? 'undefined' : OperatingSystem[actual]}"\n`
+ `UserAgent: "${testCase.userAgent}"`;
}
});

View File

@@ -16,7 +16,7 @@ describe('RuntimeEnvironment', () => {
const absentWindow = absentValue;
// act
const act = () => createEnvironment({
window: absentWindow,
window: absentWindow as never,
});
// assert
expect(act).to.throw(expectedError);
@@ -123,7 +123,7 @@ describe('RuntimeEnvironment', () => {
// arrange
const expectedValue = undefined;
const windowWithAbsentOs = {
os: absentValue,
os: absentValue as never,
};
// act
const sut = createEnvironment({

View File

@@ -12,7 +12,7 @@ describe('WindowVariablesValidator', () => {
itEachInvalidObjectValue((invalidObjectValue) => {
// arrange
const expectedError = 'window is not an object';
const window: Partial<WindowVariables> = invalidObjectValue;
const window: Partial<WindowVariables> = invalidObjectValue as never;
// act
const act = () => validateWindowVariables(window);
// assert

View File

@@ -1,21 +1,7 @@
import { describe } from 'vitest';
import { FactoryValidator, FactoryFunction } from '@/infrastructure/RuntimeSanity/Common/FactoryValidator';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
describe('FactoryValidator', () => {
describe('ctor', () => {
describe('throws when factory is absent', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing factory';
const factory = absentValue;
// act
const act = () => new TestableFactoryValidator(factory);
// assert
expect(act).to.throw(expectedError);
});
});
});
describe('collectErrors', () => {
it('reports error thrown by factory function', () => {
// arrange

View File

@@ -4,25 +4,14 @@ import { ISanityCheckOptions } from '@/infrastructure/RuntimeSanity/Common/ISani
import { SanityCheckOptionsStub } from '@tests/unit/shared/Stubs/SanityCheckOptionsStub';
import { ISanityValidator } from '@/infrastructure/RuntimeSanity/Common/ISanityValidator';
import { SanityValidatorStub } from '@tests/unit/shared/Stubs/SanityValidatorStub';
import { itEachAbsentCollectionValue, itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector';
describe('SanityChecks', () => {
describe('validateRuntimeSanity', () => {
describe('parameter validation', () => {
describe('options', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing options';
const context = new TestContext()
.withOptions(absentValue);
// act
const act = () => context.validateRuntimeSanity();
// assert
expect(act).to.throw(expectedError);
});
});
describe('throws when validators are empty', () => {
itEachAbsentCollectionValue((absentCollection) => {
itEachAbsentCollectionValue<ISanityValidator>((absentCollection) => {
// arrange
const expectedError = 'missing validators';
const validators = absentCollection;
@@ -32,20 +21,7 @@ describe('SanityChecks', () => {
const act = () => context.validateRuntimeSanity();
// assert
expect(act).to.throw(expectedError);
}, { excludeUndefined: true });
});
describe('throws when single validator is absent', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing validator in validators';
const absentValidator = absentValue;
const context = new TestContext()
.withValidators([new SanityValidatorStub(), absentValidator]);
// act
const act = () => context.validateRuntimeSanity();
// assert
expect(act).to.throw(expectedError);
});
}, { excludeUndefined: true, excludeNull: true });
});
});
@@ -97,7 +73,6 @@ describe('SanityChecks', () => {
// arrange
const firstError = 'first-error';
const secondError = 'second-error';
let actualError = '';
const context = new TestContext()
.withValidators([
new SanityValidatorStub()
@@ -105,11 +80,8 @@ describe('SanityChecks', () => {
.withErrorsResult([firstError, secondError]),
]);
// act
try {
context.validateRuntimeSanity();
} catch (err) {
actualError = err.toString();
}
const act = () => context.validateRuntimeSanity();
const actualError = collectExceptionMessage(act);
// assert
expect(actualError).to.have.length.above(0);
expect(actualError).to.include(firstError);
@@ -119,7 +91,6 @@ describe('SanityChecks', () => {
// arrange
const validatorWithErrors = 'validator-with-errors';
const validatorWithNoErrors = 'validator-with-no-errors';
let actualError = '';
const context = new TestContext()
.withValidators([
new SanityValidatorStub()
@@ -134,11 +105,8 @@ describe('SanityChecks', () => {
.withErrorsResult([]),
]);
// act
try {
context.validateRuntimeSanity();
} catch (err) {
actualError = err.toString();
}
const act = () => context.validateRuntimeSanity();
const actualError = collectExceptionMessage(act);
// assert
expect(actualError).to.have.length.above(0);
expect(actualError).to.include(validatorWithErrors);
@@ -148,7 +116,6 @@ describe('SanityChecks', () => {
// arrange
const errorFromFirstValidator = 'first-error';
const errorFromSecondValidator = 'second-error';
let actualError = '';
const context = new TestContext()
.withValidators([
new SanityValidatorStub()
@@ -159,11 +126,8 @@ describe('SanityChecks', () => {
.withErrorsResult([errorFromSecondValidator]),
]);
// act
try {
context.validateRuntimeSanity();
} catch (err) {
actualError = err.toString();
}
const act = () => context.validateRuntimeSanity();
const actualError = collectExceptionMessage(act);
// assert
expect(actualError).to.have.length.above(0);
expect(actualError).to.include(errorFromFirstValidator);

View File

@@ -13,9 +13,6 @@ interface ITestOptions<T> {
export function runFactoryValidatorTests<T>(
testOptions: ITestOptions<T>,
) {
if (!testOptions) {
throw new Error('missing options');
}
describe('shouldValidate', () => {
it('returns true when option is true', () => {
// arrange

View File

@@ -10,7 +10,7 @@ describe('WindowInjectedSystemOperations', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing window';
const window: WindowVariables = absentValue;
const window: WindowVariables = absentValue as never;
// act
const act = () => getWindowInjectedSystemOperations(window);
// assert
@@ -23,7 +23,7 @@ describe('WindowInjectedSystemOperations', () => {
const expectedError = 'missing system';
const absentSystem = absentValue;
const window: Partial<WindowVariables> = {
system: absentSystem,
system: absentSystem as never,
};
// act
const act = () => getWindowInjectedSystemOperations(window);