From 9e8fd4745f4be90d86794088f92a75bb3f070665 Mon Sep 17 00:00:00 2001 From: VonRehberg Date: Fri, 28 Jun 2024 15:11:22 +0200 Subject: [PATCH] fix(ts-sinon): fixed match all issue of jest mocks (#752) Co-authored-by: Christian Jeschke --- packages/testing/ts-jest/src/mocks.spec.ts | 40 +++++++---- packages/testing/ts-jest/src/mocks.ts | 79 ++++++++++++++++------ 2 files changed, 85 insertions(+), 34 deletions(-) diff --git a/packages/testing/ts-jest/src/mocks.spec.ts b/packages/testing/ts-jest/src/mocks.spec.ts index 1b0b88b45..682bade0c 100644 --- a/packages/testing/ts-jest/src/mocks.spec.ts +++ b/packages/testing/ts-jest/src/mocks.spec.ts @@ -8,6 +8,7 @@ interface TestInterface { someBool: boolean; optional: string | undefined; func: (num: number, str: string) => boolean; + func2: (entity: TestClass) => void; } class TestClass { @@ -41,7 +42,7 @@ describe('Mocks', () => { const result = mock.switchToHttp().getRequest(); expect(result).toBe(request); - expect(mock.switchToHttp).toBeCalledTimes(1); + expect(mock.switchToHttp).toHaveBeenCalledTimes(1); }); it('should work with truthy values properties', () => { @@ -83,8 +84,22 @@ describe('Mocks', () => { const funcResult = mock.func(42, '42'); expect(funcResult).toBe(false); - expect(mock.func).toBeCalledTimes(1); - expect(mock.func).toBeCalledWith(42, '42'); + expect(mock.func).toHaveBeenCalledTimes(1); + expect(mock.func).toHaveBeenCalledWith(42, '42'); + }); + + it('should match mocked instances', () => { + const mock = createMock(); + const mockedInstance = createMock({ someProperty: 42 }); + + mock.func2(mockedInstance); + expect(mock.func2).toHaveBeenCalledWith(mockedInstance); + + // In a previous version a bug caused all checks to pass, no matter which parameter was asserted + // These tests shall help avoiding regressions + expect(mock.func2).not.toHaveBeenCalledWith(42); + expect(mock.func2).not.toHaveBeenCalledWith('42'); + expect(mock.func2).not.toHaveBeenCalledWith(true); }); it('should work with classes', () => { @@ -121,7 +136,7 @@ describe('Mocks', () => { const result = await mock.doSomethingAsync(); expect(result).toBe(42); - expect(mock.doSomethingAsync).toBeCalledTimes(1); + expect(mock.doSomethingAsync).toHaveBeenCalledTimes(1); }); it('should work with unknown properties', () => { @@ -156,8 +171,8 @@ describe('Mocks', () => { const second = mock.switchToRpc(); const third = mock.switchToWs(); - expect(mock.switchToRpc).toBeCalledTimes(2); - expect(mock.switchToWs).toBeCalledTimes(1); + expect(mock.switchToRpc).toHaveBeenCalledTimes(2); + expect(mock.switchToWs).toHaveBeenCalledTimes(1); expect(first.getContext).toBeDefined(); expect(second.getContext).toBeDefined(); expect(third.getClient).toBeDefined(); @@ -173,7 +188,7 @@ describe('Mocks', () => { const result = executionContextMock.switchToHttp().getRequest(); expect(result).toBe(request); - expect(httpArgsHost.getRequest).toBeCalledTimes(1); + expect(httpArgsHost.getRequest).toHaveBeenCalledTimes(1); }); it('should automock promises so that they are awaitable', async () => { @@ -185,7 +200,7 @@ describe('Mocks', () => { const result = await mock.doSomethingAsync(); expect(result).toBeDefined(); - expect(mock.doSomethingAsync).toBeCalledTimes(1); + expect(mock.doSomethingAsync).toHaveBeenCalledTimes(1); }); it('should automock objects returned from automocks', () => { @@ -198,8 +213,8 @@ describe('Mocks', () => { expect(request1).toBe(request); expect(request2).toBe(request); - expect(mock.switchToHttp).toBeCalledTimes(3); - expect(mock.switchToHttp().getRequest).toBeCalledTimes(2); + expect(mock.switchToHttp).toHaveBeenCalledTimes(3); + expect(mock.switchToHttp().getRequest).toHaveBeenCalledTimes(2); }); it('should automock objects returned from automocks recursively', () => { @@ -278,8 +293,9 @@ describe('Mocks', () => { }).compile(); mockedProvider = module.get>(diToken); - dependentProvider = - module.get<{ dependent: () => string }>(dependentToken); + dependentProvider = module.get<{ dependent: () => string }>( + dependentToken + ); }); it('should correctly resolve mocked providers', async () => { diff --git a/packages/testing/ts-jest/src/mocks.ts b/packages/testing/ts-jest/src/mocks.ts index c879e6ddf..36f1001b7 100644 --- a/packages/testing/ts-jest/src/mocks.ts +++ b/packages/testing/ts-jest/src/mocks.ts @@ -26,34 +26,68 @@ export type DeepMocked = { const createRecursiveMockProxy = (name: string) => { const cache = new Map(); - const proxy = new Proxy( - {}, - { - get: (obj, prop) => { - const propName = prop.toString(); - if (cache.has(prop)) { - return cache.get(prop); + const t = jest.fn(); + return new Proxy(t, { + apply: (target, thisArg, argsArray) => { + const result = Reflect.apply(target, thisArg, argsArray); + if (result) { + return result; + } else { + if (!cache.has('__apply')) { + cache.set('__apply', createRecursiveMockProxy('bla')); } + return cache.get('__apply'); + } + }, + get: (obj, prop, receiver) => { + const propName = prop.toString(); - const checkProp = obj[prop]; + if ( + [ + '_isMockFunction', + 'mock', + 'mockClear', + 'mockImplementation', + 'mockImplementationOnce', + 'mockName', + 'getMockName', + 'getMockImplementation', + 'mockRejectedValue', + 'mockRejectedValueOnce', + 'mockReset', + 'mockResolvedValue', + 'mockResolvedValueOnce', + 'mockRestore', + 'mockReturnThis', + 'mockReturnValue', + 'mockReturnValueOnce', + 'withImplementation', + 'calls', + ].includes(propName) + ) { + return Reflect.get(obj, prop, receiver); + } - const mockedProp = - prop in obj - ? typeof checkProp === 'function' - ? jest.fn() - : checkProp - : propName === 'then' - ? undefined - : createRecursiveMockProxy(propName); + if (cache.has(prop)) { + return cache.get(prop); + } + + const checkProp = obj[prop]; - cache.set(prop, mockedProp); + const mockedProp = + prop in obj + ? typeof checkProp === 'function' + ? jest.fn() + : checkProp + : propName === 'then' + ? undefined + : createRecursiveMockProxy(propName); - return mockedProp; - }, - } - ); + cache.set(prop, mockedProp); - return jest.fn(() => proxy); + return mockedProp; + }, + }); }; export type MockOptions = { @@ -72,6 +106,7 @@ export const createMock = ( if ( prop === 'inspect' || prop === 'then' || + prop === 'asymmetricMatch' || (typeof prop === 'symbol' && prop.toString() === 'Symbol(util.inspect.custom)') ) {