Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master'
Browse files Browse the repository at this point in the history
# Conflicts:
#	package.json
  • Loading branch information
marchaos committed Mar 23, 2023
2 parents c34cdf7 + 2f42be2 commit a8228dc
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 25 deletions.
39 changes: 38 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,19 @@ describe('Party Tests', () => {

expect(mock.getPartyType()).toBe('west coast party');
});
});

test('throwing an error if we forget to specify the return value')
const mock = mock<PartyProvider>(
{},
{
fallbackMockImplementation: () => {
throw new Error('not mocked');
},
}
);

expect(() => mock.getPartyType()).toThrowError('not mocked');
});
```

## Assigning Mocks with a Type
Expand Down Expand Up @@ -128,6 +140,31 @@ const mockObj: DeepMockProxy<Test1> = mockDeep<Test1>();
mockObj.deepProp.getNumber.calledWith(1).mockReturnValue(4);
expect(mockObj.deepProp.getNumber(1)).toBe(4);
```
if you also need support for properties on functions, you can pass in an option to enable this

```ts
import { mockDeep } from 'jest-mock-extended';

const mockObj: DeepMockProxy<Test1> = mockDeep<Test1>({ funcPropSupport: true });
mockObj.deepProp.calledWith(1).mockReturnValue(3)
mockObj.deepProp.getNumber.calledWith(1).mockReturnValue(4);

expect(mockObj.deepProp(1)).toBe(3);
expect(mockObj.deepProp.getNumber(1)).toBe(4);
```

Can can provide a fallback mock implementation used if you do not define a return value using `calledWith`.

```ts
import { mockDeep } from 'jest-mock-extended';
const mockObj = mockDeep<Test1>({
fallbackMockImplementation: () => {
throw new Error('please add expected return value using calledWith');
},
});
expect(() => mockObj.getNumber()).toThrowError('not mocked');
```


## Available Matchers

Expand Down
12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "jest-mock-extended",
"version": "2.0.8",
"version": "3.0.2",
"homepage": "https://github.com/marchaos/jest-mock-extended",
"description": "Type safe mocking extensions for jest",
"files": [
Expand All @@ -25,15 +25,15 @@
"devDependencies": {
"@types/jest": "^27.5.0",
"coveralls": "^3.1.1",
"jest": "^28.1.0",
"jest": "^29.5.0",
"prettier": "^2.3.2",
"rimraf": "^3.0.2",
"ts-jest": "^28.0.1",
"typescript": "^4.3.5"
"ts-jest": "^29.0.5",
"typescript": "^5.0.2"
},
"peerDependencies": {
"jest": "^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0 || ^28.0.0",
"typescript": "^3.0.0 || ^4.0.0"
"jest": "^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0 || ^28.0.0 || ^29.0.0",
"typescript": "^3.0.0 || ^4.0.0 || ^5.0.0"
},
"author": "Marc McIntyre <[email protected]>",
"license": "MIT"
Expand Down
27 changes: 18 additions & 9 deletions src/CalledWithFn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@ function isJestAsymmetricMatcher(obj: any): obj is JestAsymmetricMatcher {
return !!obj && typeof obj === 'object' && 'asymmetricMatch' in obj && typeof obj.asymmetricMatch === 'function';
}

const checkCalledWith = <T, Y extends any[]>(calledWithStack: CalledWithStackItem<T, Y>[], actualArgs: Y): T => {
const calledWithInstance = calledWithStack.find(instance =>
const checkCalledWith = <T, Y extends any[]>(
calledWithStack: CalledWithStackItem<T, Y>[],
actualArgs: Y,
fallbackMockImplementation?: (...args: Y) => T
): T => {
const calledWithInstance = calledWithStack.find((instance) =>
instance.args.every((matcher, i) => {
if (matcher instanceof Matcher) {
return matcher.asymmetricMatch(actualArgs[i]);
Expand All @@ -29,23 +33,28 @@ const checkCalledWith = <T, Y extends any[]>(calledWithStack: CalledWithStackIte
);

// @ts-ignore cannot return undefined, but this will fail the test if there is an expectation which is what we want
return calledWithInstance ? calledWithInstance.calledWithFn(...actualArgs) : undefined;
return calledWithInstance
? calledWithInstance.calledWithFn(...actualArgs)
: fallbackMockImplementation && fallbackMockImplementation(...actualArgs);
};

export const calledWithFn = <T, Y extends any[]>(): CalledWithMock<T, Y> => {
const fn: jest.Mock<T, Y> = jest.fn();
export const calledWithFn = <T, Y extends any[]>({
fallbackMockImplementation,
}: { fallbackMockImplementation?: (...args: Y) => T } = {}): CalledWithMock<T, Y> => {
const fn: jest.Mock<T, Y> = jest.fn(fallbackMockImplementation);
let calledWithStack: CalledWithStackItem<T, Y>[] = [];

(fn as CalledWithMock<T, Y>).calledWith = (...args) => {
// We create new function to delegate any interactions (mockReturnValue etc.) to for this set of args.
// If that set of args is matched, we just call that jest.fn() for the result.
const calledWithFn = jest.fn();
if (!fn.getMockImplementation()) {
const calledWithFn = jest.fn(fallbackMockImplementation);
const mockImplementation = fn.getMockImplementation();
if (!mockImplementation || mockImplementation === fallbackMockImplementation) {
// Our original function gets a mock implementation which handles the matching
fn.mockImplementation((...args: Y) => checkCalledWith(calledWithStack, args));
fn.mockImplementation((...args: Y) => checkCalledWith(calledWithStack, args, fallbackMockImplementation));
calledWithStack = [];
}
calledWithStack.push({ args, calledWithFn });
calledWithStack.unshift({ args, calledWithFn });

return calledWithFn;
};
Expand Down
59 changes: 56 additions & 3 deletions src/Mock.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,19 @@ describe('jest-mock-extended', () => {
expect(mockObj.getSomethingWithArgs(1, 2)).toBe(1);
});

test('Can specify fallbackMockImplementation', () => {
const mockObj = mock<MockInt>(
{},
{
fallbackMockImplementation: () => {
throw new Error('not mocked');
},
}
);

expect(() => mockObj.getSomethingWithArgs(1, 2)).toThrowError('not mocked');
});

test('Can specify multiple calledWith', () => {
const mockObj = mock<MockInt>();
mockObj.getSomethingWithArgs.calledWith(1, 2).mockReturnValue(3);
Expand Down Expand Up @@ -225,6 +238,14 @@ describe('jest-mock-extended', () => {
expect(mockObj.getSomethingWithArgs(7, 2)).toBe(undefined);
});

test('supports overriding with same args', () => {
const mockObj = mock<MockInt>();
mockObj.getSomethingWithArgs.calledWith(1, 2).mockReturnValue(4);
mockObj.getSomethingWithArgs.calledWith(1, 2).mockReturnValue(3);

expect(mockObj.getSomethingWithArgs(1, 2)).toBe(3);
});

test('Support jest matcher', () => {
const mockObj = mock<MockInt>();
mockObj.getSomethingWithArgs.calledWith(expect.anything(), expect.anything()).mockReturnValue(3);
Expand Down Expand Up @@ -300,26 +321,58 @@ describe('jest-mock-extended', () => {
mockObj.deepProp.getNumber(2);
expect(mockObj.deepProp.getNumber).toHaveBeenCalledTimes(1);
});

test('fallback mock implementation can be overridden', () => {
const mockObj = mockDeep<Test1>({
fallbackMockImplementation: () => {
throw new Error('not mocked');
},
});
mockObj.deepProp.getAnotherString.calledWith('foo'); // no mock implementation
expect(() => mockObj.getNumber()).toThrowError('not mocked');
expect(() => mockObj.deepProp.getAnotherString('foo')).toThrowError('not mocked');
});

test('fallback mock implementation can be overridden while also providing a mock implementation', () => {
const mockObj = mockDeep<Test1>(
{
fallbackMockImplementation: () => {
throw new Error('not mocked');
},
},
{
getNumber: () => {
return 150;
},
}
);
mockObj.deepProp.getAnotherString.calledWith('?').mockReturnValue('mocked');
expect(mockObj.getNumber()).toBe(150);
expect(mockObj.deepProp.getAnotherString('?')).toBe('mocked');

expect(() => mockObj.deepProp.getNumber(1)).toThrowError('not mocked');
expect(() => mockObj.deepProp.getAnotherString('!')).toThrowError('not mocked');
});
});

describe('Deep mock support for class variables which are functions but also have nested properties and functions', () => {
test('can deep mock members', () => {
const mockObj = mockDeep<Test6>();
const mockObj = mockDeep<Test6>({ funcPropSupport: true });
const input = new Test1(1);
mockObj.funcValueProp.nonDeepProp.calledWith(input).mockReturnValue(4);

expect(mockObj.funcValueProp.nonDeepProp(input)).toBe(4);
});

test('three or more level deep mock', () => {
const mockObj = mockDeep<Test6>();
const mockObj = mockDeep<Test6>({ funcPropSupport: true });
mockObj.funcValueProp.deepProp.deeperProp.getNumber.calledWith(1).mockReturnValue(4);

expect(mockObj.funcValueProp.deepProp.deeperProp.getNumber(1)).toBe(4);
});

test('maintains API for deep mocks', () => {
const mockObj = mockDeep<Test6>();
const mockObj = mockDeep<Test6>({ funcPropSupport: true });
mockObj.funcValueProp.deepProp.getNumber(100);

expect(mockObj.funcValueProp.deepProp.getNumber.mock.calls[0][0]).toBe(100);
Expand Down
31 changes: 25 additions & 6 deletions src/Mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,25 @@ export interface CalledWithMock<T, Y extends any[]> extends jest.Mock<T, Y> {
}

export type MockProxy<T> = {
// This supports deep mocks in the else branch
[K in keyof T]: T[K] extends (...args: infer A) => infer B ? CalledWithMock<B, A> : T[K];
} & T;
} &
T;

export type DeepMockProxy<T> = {
// This supports deep mocks in the else branch
[K in keyof T]: T[K] extends (...args: infer A) => infer B ? CalledWithMock<B, A> : DeepMockProxy<T[K]>;
} &
T;

export type DeepMockProxyWithFuncPropSupport<T> = {
// This supports deep mocks in the else branch
[K in keyof T]: T[K] extends (...args: infer A) => infer B ? CalledWithMock<B, A> & DeepMockProxy<T[K]> : DeepMockProxy<T[K]>;
} & T;
} &
T;

export interface MockOpts {
deep?: boolean;
fallbackMockImplementation?: (...args: any[]) => any;
}

export const mockClear = (mock: MockProxy<any>) => {
Expand Down Expand Up @@ -87,7 +95,18 @@ export const mockReset = (mock: MockProxy<any>) => {
}
};

export const mockDeep = <T>(mockImplementation?: DeepPartial<T>) => mock<T, DeepMockProxy<T> & T>(mockImplementation, { deep: true });
export function mockDeep<T>(
opts: { funcPropSupport?: true; fallbackMockImplementation?: MockOpts['fallbackMockImplementation'] },
mockImplementation?: DeepPartial<T>
): DeepMockProxyWithFuncPropSupport<T>;
export function mockDeep<T>(mockImplementation?: DeepPartial<T>): DeepMockProxy<T>;
export function mockDeep(arg1: any, arg2?: any) {
const [opts, mockImplementation] =
typeof arg1 === 'object' && (typeof arg1.fallbackMockImplementation === 'function' || arg1.funcPropSupport === true)
? [arg1, arg2]
: [{}, arg1];
return mock(mockImplementation, { deep: true, fallbackMockImplementation: opts.fallbackMockImplementation });
}

const overrideMockImp = (obj: DeepPartial<any>, opts?: MockOpts) => {
const proxy = new Proxy<MockProxy<any>>(obj, handler(opts));
Expand All @@ -114,7 +133,7 @@ const handler = (opts?: MockOpts) => ({
},

get: (obj: MockProxy<any>, property: ProxiedProperty) => {
let fn = calledWithFn();
let fn = calledWithFn({ fallbackMockImplementation: opts?.fallbackMockImplementation });

// @ts-ignore
if (!(property in obj)) {
Expand All @@ -137,7 +156,7 @@ const handler = (opts?: MockOpts) => ({
obj[property]._isMockObject = true;
} else {
// @ts-ignore
obj[property] = calledWithFn();
obj[property] = calledWithFn({ fallbackMockImplementation: opts?.fallbackMockImplementation });
}
}

Expand Down

0 comments on commit a8228dc

Please sign in to comment.