Skip to content

Commit

Permalink
feat(core): support primitive value in testbuilder api (#59)
Browse files Browse the repository at this point in the history
- Change "reflectors" terminology to "adapters"
- Support primitive value in fluent api in .using() method
  • Loading branch information
omermorad committed Jun 9, 2023
1 parent eb2cc76 commit 50cbb99
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 33 deletions.
11 changes: 8 additions & 3 deletions packages/core/__test__/testbed-builder.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,24 @@ import {
DependencyTwo,
DependencyFive,
} from './integration.assets';
import { PrimitiveValue } from '@automock/common';

describe('Builder Factory Integration Test', () => {
let underTest: TestBedBuilder<MainClass>;

// It's a function that mocks the mock function, don't be confused by the name
// It's a mark for a function that mocks the mock function, don't be confused by the name
const mockFunctionMockOfBuilder = jest.fn(() => '__MOCKED_FROM_BUILDER__');
const mockFunctionMockOfMocker = jest.fn(() => '__MOCKED_FROM_MOCKER__');

const reflectorMock = {
reflectDependencies: () => {
return new Map<Type | string, Type>([
return new Map<Type | string, Type | PrimitiveValue>([
[DependencyOne, DependencyOne],
[DependencyTwo, DependencyTwo],
[DependencyThree, DependencyThree],
['DEPENDENCY_FOUR_TOKEN', DependencyFourToken],
[DependencyFive, DependencyFive],
['STRING_TOKEN', 'ANY STRING'],
]);
},
};
Expand Down Expand Up @@ -55,6 +57,8 @@ describe('Builder Factory Integration Test', () => {
.using({
print: () => 'dependency-four-overridden',
})
.mock<string>('STRING_TOKEN')
.using('ARBITRARY_STRING')
.compile();
});

Expand All @@ -73,8 +77,9 @@ describe('Builder Factory Integration Test', () => {
[DependencyOne.name, '__MOCKED_FROM_BUILDER__', DependencyOne],
[DependencyTwo.name, '__MOCKED_FROM_BUILDER__', DependencyTwo],
[DependencyThree.name, '__MOCKED_FROM_MOCKER__', DependencyThree],
['DEPENDENCY_FOUR_TOKEN', '__MOCKED_FROM_BUILDER__', 'DEPENDENCY_FOUR_TOKEN'],
['custom token with function', '__MOCKED_FROM_BUILDER__', 'DEPENDENCY_FOUR_TOKEN'],
[DependencyFive.name, '__MOCKED_FROM_MOCKER__', DependencyFive],
['custom token with primitive value', 'ARBITRARY_STRING', 'STRING_TOKEN'],
])(
'should return a stubbed instance for %p, mocked from %p',
(name: string, expectedResult: Type | string, dependency: Type | string) => {
Expand Down
16 changes: 8 additions & 8 deletions packages/core/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import { MockFunction, Type } from '@automock/types';
import { AutomockReflectors, PackageResolver } from './services/package-resolver';
import { AutomockAdapters, PackageResolver } from './services/package-resolver';
import { DependenciesMocker } from './services/dependencies-mocker';
import { BuilderFactory, TestBedBuilder } from './services/testbed-builder';

export function createTestbedBuilder<TClass>(
mockFn: MockFunction<unknown>,
reflectors: Record<string, string>
adapters: Record<string, string>
): (targetClass: Type<TClass>) => TestBedBuilder<TClass> {
try {
const packageResolver = new PackageResolver(reflectors, {
const packageResolver = new PackageResolver(adapters, {
resolve: require.resolve,
require,
});

const reflector = packageResolver.resolveCorrespondingReflector();
const dependenciesMapper = new DependenciesMocker(reflector, mockFn);
const adapter = packageResolver.resolveCorrespondingAdapter();
const dependenciesMapper = new DependenciesMocker(adapter, mockFn);

return BuilderFactory.create<TClass>(mockFn, dependenciesMapper);
} catch (error: unknown) {
throw new Error(
`No corresponding reflector found. Please make sure to install one of the following reflector packages: ${Object.keys(
reflectors
`No corresponding adapter found. Please make sure to install one of the following adapters packages: ${Object.keys(
adapters
).join(
', '
)}. Refer to the documentation for further information: https://github.com/automock/automock#installation.`
Expand All @@ -31,4 +31,4 @@ export function createTestbedBuilder<TClass>(
export const AutomockTestBuilder = <TClass>(
mockFn: MockFunction<unknown>
): ((targetClass: Type<TClass>) => TestBedBuilder<TClass>) =>
createTestbedBuilder(mockFn, AutomockReflectors);
createTestbedBuilder(mockFn, AutomockAdapters);
4 changes: 2 additions & 2 deletions packages/core/src/services/dependencies-mocker.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Type, MockFunction, StubbedInstance } from '@automock/types';
import { DependenciesReflector } from '@automock/common';
import { DependenciesReflector, PrimitiveValue } from '@automock/common';

export class DependenciesMocker {
public constructor(
Expand All @@ -16,7 +16,7 @@ export class DependenciesMocker {
public mockAllDependencies<TClass>(
targetClass: Type<TClass>
): (
alreadyMockedDependencies: Map<Type | string, StubbedInstance<unknown>>
alreadyMockedDependencies: Map<Type | string, StubbedInstance<unknown> | PrimitiveValue>
) => Map<Type | string, StubbedInstance<unknown>> {
return (alreadyMockedDependencies: Map<Type | string, StubbedInstance<unknown>>) => {
const classDependencies = this.reflector.reflectDependencies(targetClass);
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/services/package-resolver.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('Package Resolving Unit Spec', () => {
test('require, and return the module if the a resolver was found', () => {
requireMock.resolve.mockReturnValue('valid');
requireMock.require.mockReturnValue('IS_A_VALID_MODULE');
const reflector = underTest.resolveCorrespondingReflector();
const reflector = underTest.resolveCorrespondingAdapter();

expect(requireMock.require).toHaveBeenCalledWith('first-reflector');
expect(reflector).toBe('IS_A_VALID_MODULE');
Expand All @@ -32,7 +32,7 @@ describe('Package Resolving Unit Spec', () => {
});

requireMock.require.mockReturnValue('IS_A_VALID_MODULE');
expect(() => underTest.resolveCorrespondingReflector()).toThrow();
expect(() => underTest.resolveCorrespondingAdapter()).toThrow();
expect(requireMock.require).not.toHaveBeenCalled();
});
});
18 changes: 9 additions & 9 deletions packages/core/src/services/package-resolver.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { DependenciesReflector } from '@automock/common';

export const AutomockReflectors: Record<string, string> = {
export const AutomockAdapters: Record<string, string> = {
nestjs: '@automock/adapters.nestjs',
} as const;

Expand All @@ -11,22 +11,22 @@ interface NodeRequire {

export class PackageResolver {
public constructor(
private readonly reflectors: Record<string, string>,
private readonly adapters: Record<string, string>,
private readonly require: NodeRequire
) {}

public resolveCorrespondingReflector(): DependenciesReflector | never {
const resolvers = Object.keys(this.reflectors);
public resolveCorrespondingAdapter(): DependenciesReflector | never {
const resolvers = Object.keys(this.adapters);

const reflectorName = resolvers.find((resolverName: string) =>
this.packageIsAvailable(this.reflectors[resolverName])
const adapterName = resolvers.find((resolverName: string) =>
this.packageIsAvailable(this.adapters[resolverName])
);

if (!reflectorName) {
throw new Error('No corresponding reflector found');
if (!adapterName) {
throw new Error('No corresponding adapter found');
}

return this.require.require(this.reflectors[reflectorName]) as DependenciesReflector;
return this.require.require(this.adapters[adapterName]) as DependenciesReflector;
}

private packageIsAvailable(path: string): boolean {
Expand Down
62 changes: 53 additions & 9 deletions packages/core/src/services/testbed-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,40 @@ import { DeepPartial, Type, MockFunction, StubbedInstance } from '@automock/type
import { UnitReference } from './unit-reference';
import { DependenciesMocker } from './dependencies-mocker';
import { UnitTestBed } from '../types';
import { PrimitiveValue } from '@automock/common';

/**
* Represents an override configuration for a mocked dependency.
*/
export interface MockOverride<TDep, TClass> {
/**
* Specifies the mock implementation to be used for the mocked object.
* @param mockImplementation - The current implementation (either complete or partial)
* of the mocked object.
* Specifies the value to be used for the mocked dependency.
*
* @returns A TestBedBuilder instance for chaining further configuration.
* @template Impl - The type of the mock implementation.
* @param value - The value for the mocked dependency.
* @returns A `TestBedBuilder` instance for chaining further configuration.
* @template TValue - The type of the value.
*/
using<Impl extends DeepPartial<TDep>>(mockImplementation: Impl): TestBedBuilder<TClass>;
using<TValue extends PrimitiveValue>(value: TValue): TestBedBuilder<TClass>;

/**
* Specifies the mock implementation to be used for the mocked dependency.
*
* @param mockImplementation - The mock implementation for the mocked dependency.
* @returns A `TestBedBuilder` instance for chaining further configuration.
* @template TImpl - The type of the mock implementation.
*/
using<TImpl extends DeepPartial<TDep>>(mockImplementation: TImpl): TestBedBuilder<TClass>;

/**
* Specifies the mock implementation or value to be used for the mocked dependency.
*
* @param mockImplementationOrValue - The mock implementation or value.
* @returns A `TestBedBuilder` instance for chaining further configuration.
* @template TImpl - The type of the mock implementation or value.
*/
using<TImpl extends DeepPartial<TDep> | PrimitiveValue>(
mockImplementationOrValue: TImpl
): TestBedBuilder<TClass>;
}

export interface TestBedBuilder<TClass> {
Expand Down Expand Up @@ -63,17 +86,28 @@ export class BuilderFactory {
): (targetClass: Type<TClass>) => TestBedBuilder<TClass> {
return (targetClass: Type<TClass>): TestBedBuilder<TClass> => {
const instance = new this(mockFn, dependenciesMocker);
const dependenciesToOverride = new Map<Type | string, StubbedInstance<unknown>>();
const dependenciesToOverride = new Map<
Type | string,
PrimitiveValue | StubbedInstance<unknown>
>();

return {
mock<TDependency>(
typeOrToken: string | Type<TDependency>
): MockOverride<TDependency, TClass> {
return {
using: (mockImplementation: DeepPartial<TDependency>) => {
using: (mockImplementationOrValue: DeepPartial<TDependency> | PrimitiveValue) => {
if (isPrimitive(mockImplementationOrValue)) {
dependenciesToOverride.set(
typeOrToken,
mockImplementationOrValue as PrimitiveValue
);
return this;
}

dependenciesToOverride.set(
typeOrToken,
instance.mockFn(mockImplementation) as StubbedInstance<TDependency>
instance.mockFn(mockImplementationOrValue) as StubbedInstance<TDependency>
);
return this;
},
Expand All @@ -94,3 +128,13 @@ export class BuilderFactory {
};
}
}

function isPrimitive(value: unknown): value is PrimitiveValue {
return (
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean' ||
typeof value === 'symbol' ||
value === null
);
}

0 comments on commit 50cbb99

Please sign in to comment.