Skip to content

Commit

Permalink
feat(sinon): replace @golevelup/ts-sinon with native mocking function…
Browse files Browse the repository at this point in the history
…ality

Removed @golevelup/ts-sinon, added native auto mocking functionality instead
  • Loading branch information
omermorad committed Nov 7, 2023
1 parent 9e047bb commit 080117b
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 6 deletions.
3 changes: 1 addition & 2 deletions packages/testbeds/sinon/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions packages/testbeds/sinon/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -62,4 +62,4 @@ export interface UnitTestBed<TClass> {
/**
* @deprecated Will be removed in the next major version.
*/
export type MockFunction = typeof createMock;
export type MockFunction = typeof mock;
137 changes: 137 additions & 0 deletions packages/testbeds/sinon/src/mock.spec.ts
Original file line number Diff line number Diff line change
@@ -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<TestClass>();
new TestClass(1).ofAnother(mockObject);
expect(mockObject.getNumber.callCount).toBe(1);
});

test('should create jest.fn() without any invocation', () => {
const mockObject = mock<ArbitraryMock>();
expect(mockObject.getNumber.callCount).toBe(0);
});

test('should register invocations correctly', () => {
const mockObject = mock<ArbitraryMock>();
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<ArbitraryMock>();
mockObject.getNumber.returns(12);
expect(mockObject.getNumber()).toBe(12);
});

test('should allow specifying arguments', () => {
const mockObject = mock<ArbitraryMock>();
mockObject.getSomethingWithArgs(1, 2);
expect(mockObject.getSomethingWithArgs.calledWithExactly(1, 2)).toBeTruthy();
});
});

describe('mock properties', () => {
test('should allow setting properties', () => {
const mockObject = mock<ArbitraryMock>();
mockObject.id = 17;
expect(mockObject.id).toBe(17);
});

test('should allow setting boolean properties to false or null', () => {
const mockObject = mock<ArbitraryMock>({ someValue: false });
const mockObj2 = mock<ArbitraryMock>({ someValue: null });
expect(mockObject.someValue).toBe(false);
expect(mockObj2.someValue).toBe(null);
});

test('should allow setting properties to undefined explicitly', () => {
const mockObject = mock<ArbitraryMock>({ someValue: undefined });
expect(mockObject.someValue).toBe(undefined);
});
});

describe('mock implementation', () => {
test('should allow providing mock implementations for properties', () => {
const mockObject = mock<TestClass>({ id: 61 });
expect(mockObject.id).toBe(61);
});

test('should allow providing mock implementations for functions', () => {
const mockObject = mock<TestClass>({ getNumber: () => 150 });
expect(mockObject.getNumber()).toBe(150);
});
});

describe('promises', () => {
test('should successfully use mock for promises resolving', async () => {
const mockObject = mock<ArbitraryMock>();
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<Error>();
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);
});
});
});
43 changes: 43 additions & 0 deletions packages/testbeds/sinon/src/mock.static.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { SinonStubbedInstance, stub } from 'sinon';

type PropertyType = string | number | symbol;

const createHandler = () => ({
get: (target: SinonStubbedInstance<any>, 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<any>).bind(target);
}

return target[property as string];
},
});

const applyMockImplementation = (initialObject: Record<string, any>) => {
const proxy = new Proxy<SinonStubbedInstance<any>>(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 = <T>(mockImpl: Partial<T> = {} as Partial<T>): SinonStubbedInstance<T> => {
return applyMockImplementation(mockImpl);
};
4 changes: 2 additions & 2 deletions packages/testbeds/sinon/src/testbed-factory.ts
Original file line number Diff line number Diff line change
@@ -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 {
/**
Expand All @@ -10,6 +10,6 @@ export class TestBed {
* @return TestBedBuilder
*/
public static create<TClass = any>(targetClass: Type<TClass>): TestBedBuilder<TClass> {
return AutomockTestBuilder<TClass>(createMock)(targetClass);
return AutomockTestBuilder<TClass>(mock)(targetClass);
}
}

0 comments on commit 080117b

Please sign in to comment.