From 080117b07738f25d4e9e6428159834a848e3e2cf Mon Sep 17 00:00:00 2001 From: omer Date: Tue, 31 Oct 2023 19:01:12 +0200 Subject: [PATCH] feat(sinon): replace @golevelup/ts-sinon with native mocking functionality Removed @golevelup/ts-sinon, added native auto mocking functionality instead --- packages/testbeds/sinon/package.json | 3 +- packages/testbeds/sinon/src/index.ts | 4 +- packages/testbeds/sinon/src/mock.spec.ts | 137 ++++++++++++++++++ packages/testbeds/sinon/src/mock.static.ts | 43 ++++++ .../testbeds/sinon/src/testbed-factory.ts | 4 +- 5 files changed, 185 insertions(+), 6 deletions(-) create mode 100644 packages/testbeds/sinon/src/mock.spec.ts create mode 100644 packages/testbeds/sinon/src/mock.static.ts diff --git a/packages/testbeds/sinon/package.json b/packages/testbeds/sinon/package.json index 6c90a21f..12f3e7a7 100644 --- a/packages/testbeds/sinon/package.json +++ b/packages/testbeds/sinon/package.json @@ -39,8 +39,7 @@ "dependencies": { "@automock/adapters.nestjs": "^1.4.0", "@automock/core": "^1.4.0", - "@automock/types": "^1.2.0", - "@golevelup/ts-sinon": "^0.1.0" + "@automock/types": "^1.2.0" }, "devDependencies": { "@nestjs/common": "^8.3.1", diff --git a/packages/testbeds/sinon/src/index.ts b/packages/testbeds/sinon/src/index.ts index 729a5b74..15c32111 100644 --- a/packages/testbeds/sinon/src/index.ts +++ b/packages/testbeds/sinon/src/index.ts @@ -2,7 +2,7 @@ import { Type as TypeFromTypes } from '@automock/types'; import { UnitReference } from '@automock/core'; -import { createMock } from '@golevelup/ts-sinon'; +import { mock } from './mock.static'; import { SinonStubbedInstance } from 'sinon'; export * from './testbed-factory'; @@ -62,4 +62,4 @@ export interface UnitTestBed { /** * @deprecated Will be removed in the next major version. */ -export type MockFunction = typeof createMock; +export type MockFunction = typeof mock; diff --git a/packages/testbeds/sinon/src/mock.spec.ts b/packages/testbeds/sinon/src/mock.spec.ts new file mode 100644 index 00000000..d29b3f08 --- /dev/null +++ b/packages/testbeds/sinon/src/mock.spec.ts @@ -0,0 +1,137 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { mock } from './mock.static'; + +interface ArbitraryMock { + id: number; + someValue?: boolean | null; + getNumber: () => number; + getNumberWithMockArg: (mock: never) => number; + getSomethingWithArgs: (arg1: number, arg2: number) => number; + getSomethingWithMoreArgs: (arg1: number, arg2: number, arg3: number) => number; +} + +class TestClass implements ArbitraryMock { + readonly id: number; + + constructor(id: number) { + this.id = id; + } + + public ofAnother(test: TestClass) { + return test.getNumber(); + } + + public getNumber() { + return this.id; + } + + public getNumberWithMockArg(mock: never) { + return this.id; + } + + public getSomethingWithArgs(arg1: number, arg2: number) { + return this.id; + } + + public getSomethingWithMoreArgs(arg1: number, arg2: number, arg3: number) { + return this.id; + } +} + +describe('Mocking Proxy Mechanism Unit Spec', () => { + describe('basic functionality', () => { + test('should allow assignment to itself even with private parts', () => { + const mockObject = mock(); + new TestClass(1).ofAnother(mockObject); + expect(mockObject.getNumber.callCount).toBe(1); + }); + + test('should create jest.fn() without any invocation', () => { + const mockObject = mock(); + expect(mockObject.getNumber.callCount).toBe(0); + }); + + test('should register invocations correctly', () => { + const mockObject = mock(); + mockObject.getNumber(); + mockObject.getNumber(); + expect(mockObject.getNumber.callCount).toBe(2); + }); + }); + + describe('mock return values and arguments', () => { + test('should allow mocking a return value', () => { + const mockObject = mock(); + mockObject.getNumber.returns(12); + expect(mockObject.getNumber()).toBe(12); + }); + + test('should allow specifying arguments', () => { + const mockObject = mock(); + mockObject.getSomethingWithArgs(1, 2); + expect(mockObject.getSomethingWithArgs.calledWithExactly(1, 2)).toBeTruthy(); + }); + }); + + describe('mock properties', () => { + test('should allow setting properties', () => { + const mockObject = mock(); + mockObject.id = 17; + expect(mockObject.id).toBe(17); + }); + + test('should allow setting boolean properties to false or null', () => { + const mockObject = mock({ someValue: false }); + const mockObj2 = mock({ someValue: null }); + expect(mockObject.someValue).toBe(false); + expect(mockObj2.someValue).toBe(null); + }); + + test('should allow setting properties to undefined explicitly', () => { + const mockObject = mock({ someValue: undefined }); + expect(mockObject.someValue).toBe(undefined); + }); + }); + + describe('mock implementation', () => { + test('should allow providing mock implementations for properties', () => { + const mockObject = mock({ id: 61 }); + expect(mockObject.id).toBe(61); + }); + + test('should allow providing mock implementations for functions', () => { + const mockObject = mock({ getNumber: () => 150 }); + expect(mockObject.getNumber()).toBe(150); + }); + }); + + describe('promises', () => { + test('should successfully use mock for promises resolving', async () => { + const mockObject = mock(); + mockObject.id = 17; + const promiseMockObj = Promise.resolve(mockObject); + + await expect(promiseMockObj).resolves.toBeDefined(); + await expect(promiseMockObj).resolves.toMatchObject({ id: 17 }); + }); + + test('should successfully use mock for promises rejecting', async () => { + const mockError = mock(); + mockError.message = '17'; + const promiseMockObj = Promise.reject(mockError); + + await expect(promiseMockObj).rejects.toBeDefined(); + await expect(promiseMockObj).rejects.toBe(mockError); + await expect(promiseMockObj).rejects.toHaveProperty('message', '17'); + }); + }); + + describe('mocking a date objects', () => { + test('should allow calling native date object methods', () => { + const mockObject = mock({ date: new Date('2000-01-15') }); + expect(mockObject.date.getFullYear()).toBe(2000); + expect(mockObject.date.getMonth()).toBe(0); + expect(mockObject.date.getDate()).toBe(15); + }); + }); +}); diff --git a/packages/testbeds/sinon/src/mock.static.ts b/packages/testbeds/sinon/src/mock.static.ts new file mode 100644 index 00000000..b02d9464 --- /dev/null +++ b/packages/testbeds/sinon/src/mock.static.ts @@ -0,0 +1,43 @@ +import { SinonStubbedInstance, stub } from 'sinon'; + +type PropertyType = string | number | symbol; + +const createHandler = () => ({ + get: (target: SinonStubbedInstance, property: PropertyType) => { + if (!(property in target)) { + if (property === 'then') { + return undefined; + } + + if (property === Symbol.iterator) { + return target[property as never]; + } + + target[property as string] = stub(); + } + + if (target instanceof Date && typeof target[property as never] === 'function') { + return (target[property as never] as SinonStubbedInstance).bind(target); + } + + return target[property as string]; + }, +}); + +const applyMockImplementation = (initialObject: Record) => { + const proxy = new Proxy>(initialObject, createHandler()); + + for (const key of Object.keys(initialObject)) { + if (typeof initialObject[key] === 'object' && initialObject[key] !== null) { + proxy[key] = applyMockImplementation(initialObject[key]); + } else { + proxy[key] = initialObject[key]; + } + } + + return proxy; +}; + +export const mock = (mockImpl: Partial = {} as Partial): SinonStubbedInstance => { + return applyMockImplementation(mockImpl); +}; diff --git a/packages/testbeds/sinon/src/testbed-factory.ts b/packages/testbeds/sinon/src/testbed-factory.ts index 2702544c..7a0a917d 100644 --- a/packages/testbeds/sinon/src/testbed-factory.ts +++ b/packages/testbeds/sinon/src/testbed-factory.ts @@ -1,6 +1,6 @@ import { AutomockTestBuilder, TestBedBuilder } from '@automock/core'; import { Type } from '@automock/types'; -import { createMock } from '@golevelup/ts-sinon'; +import { mock } from './mock.static'; export class TestBed { /** @@ -10,6 +10,6 @@ export class TestBed { * @return TestBedBuilder */ public static create(targetClass: Type): TestBedBuilder { - return AutomockTestBuilder(createMock)(targetClass); + return AutomockTestBuilder(mock)(targetClass); } }