Skip to content

Commit a8228dc

Browse files
committed
Merge remote-tracking branch 'origin/master'
# Conflicts: # package.json
2 parents c34cdf7 + 2f42be2 commit a8228dc

File tree

5 files changed

+143
-25
lines changed

5 files changed

+143
-25
lines changed

README.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,19 @@ describe('Party Tests', () => {
5050

5151
expect(mock.getPartyType()).toBe('west coast party');
5252
});
53-
});
53+
54+
test('throwing an error if we forget to specify the return value')
55+
const mock = mock<PartyProvider>(
56+
{},
57+
{
58+
fallbackMockImplementation: () => {
59+
throw new Error('not mocked');
60+
},
61+
}
62+
);
63+
64+
expect(() => mock.getPartyType()).toThrowError('not mocked');
65+
});
5466
```
5567

5668
## Assigning Mocks with a Type
@@ -128,6 +140,31 @@ const mockObj: DeepMockProxy<Test1> = mockDeep<Test1>();
128140
mockObj.deepProp.getNumber.calledWith(1).mockReturnValue(4);
129141
expect(mockObj.deepProp.getNumber(1)).toBe(4);
130142
```
143+
if you also need support for properties on functions, you can pass in an option to enable this
144+
145+
```ts
146+
import { mockDeep } from 'jest-mock-extended';
147+
148+
const mockObj: DeepMockProxy<Test1> = mockDeep<Test1>({ funcPropSupport: true });
149+
mockObj.deepProp.calledWith(1).mockReturnValue(3)
150+
mockObj.deepProp.getNumber.calledWith(1).mockReturnValue(4);
151+
152+
expect(mockObj.deepProp(1)).toBe(3);
153+
expect(mockObj.deepProp.getNumber(1)).toBe(4);
154+
```
155+
156+
Can can provide a fallback mock implementation used if you do not define a return value using `calledWith`.
157+
158+
```ts
159+
import { mockDeep } from 'jest-mock-extended';
160+
const mockObj = mockDeep<Test1>({
161+
fallbackMockImplementation: () => {
162+
throw new Error('please add expected return value using calledWith');
163+
},
164+
});
165+
expect(() => mockObj.getNumber()).toThrowError('not mocked');
166+
```
167+
131168

132169
## Available Matchers
133170

package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "jest-mock-extended",
3-
"version": "2.0.8",
3+
"version": "3.0.2",
44
"homepage": "https://github.com/marchaos/jest-mock-extended",
55
"description": "Type safe mocking extensions for jest",
66
"files": [
@@ -25,15 +25,15 @@
2525
"devDependencies": {
2626
"@types/jest": "^27.5.0",
2727
"coveralls": "^3.1.1",
28-
"jest": "^28.1.0",
28+
"jest": "^29.5.0",
2929
"prettier": "^2.3.2",
3030
"rimraf": "^3.0.2",
31-
"ts-jest": "^28.0.1",
32-
"typescript": "^4.3.5"
31+
"ts-jest": "^29.0.5",
32+
"typescript": "^5.0.2"
3333
},
3434
"peerDependencies": {
35-
"jest": "^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0 || ^28.0.0",
36-
"typescript": "^3.0.0 || ^4.0.0"
35+
"jest": "^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0 || ^28.0.0 || ^29.0.0",
36+
"typescript": "^3.0.0 || ^4.0.0 || ^5.0.0"
3737
},
3838
"author": "Marc McIntyre <[email protected]>",
3939
"license": "MIT"

src/CalledWithFn.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@ function isJestAsymmetricMatcher(obj: any): obj is JestAsymmetricMatcher {
1313
return !!obj && typeof obj === 'object' && 'asymmetricMatch' in obj && typeof obj.asymmetricMatch === 'function';
1414
}
1515

16-
const checkCalledWith = <T, Y extends any[]>(calledWithStack: CalledWithStackItem<T, Y>[], actualArgs: Y): T => {
17-
const calledWithInstance = calledWithStack.find(instance =>
16+
const checkCalledWith = <T, Y extends any[]>(
17+
calledWithStack: CalledWithStackItem<T, Y>[],
18+
actualArgs: Y,
19+
fallbackMockImplementation?: (...args: Y) => T
20+
): T => {
21+
const calledWithInstance = calledWithStack.find((instance) =>
1822
instance.args.every((matcher, i) => {
1923
if (matcher instanceof Matcher) {
2024
return matcher.asymmetricMatch(actualArgs[i]);
@@ -29,23 +33,28 @@ const checkCalledWith = <T, Y extends any[]>(calledWithStack: CalledWithStackIte
2933
);
3034

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

35-
export const calledWithFn = <T, Y extends any[]>(): CalledWithMock<T, Y> => {
36-
const fn: jest.Mock<T, Y> = jest.fn();
41+
export const calledWithFn = <T, Y extends any[]>({
42+
fallbackMockImplementation,
43+
}: { fallbackMockImplementation?: (...args: Y) => T } = {}): CalledWithMock<T, Y> => {
44+
const fn: jest.Mock<T, Y> = jest.fn(fallbackMockImplementation);
3745
let calledWithStack: CalledWithStackItem<T, Y>[] = [];
3846

3947
(fn as CalledWithMock<T, Y>).calledWith = (...args) => {
4048
// We create new function to delegate any interactions (mockReturnValue etc.) to for this set of args.
4149
// If that set of args is matched, we just call that jest.fn() for the result.
42-
const calledWithFn = jest.fn();
43-
if (!fn.getMockImplementation()) {
50+
const calledWithFn = jest.fn(fallbackMockImplementation);
51+
const mockImplementation = fn.getMockImplementation();
52+
if (!mockImplementation || mockImplementation === fallbackMockImplementation) {
4453
// Our original function gets a mock implementation which handles the matching
45-
fn.mockImplementation((...args: Y) => checkCalledWith(calledWithStack, args));
54+
fn.mockImplementation((...args: Y) => checkCalledWith(calledWithStack, args, fallbackMockImplementation));
4655
calledWithStack = [];
4756
}
48-
calledWithStack.push({ args, calledWithFn });
57+
calledWithStack.unshift({ args, calledWithFn });
4958

5059
return calledWithFn;
5160
};

src/Mock.spec.ts

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,19 @@ describe('jest-mock-extended', () => {
124124
expect(mockObj.getSomethingWithArgs(1, 2)).toBe(1);
125125
});
126126

127+
test('Can specify fallbackMockImplementation', () => {
128+
const mockObj = mock<MockInt>(
129+
{},
130+
{
131+
fallbackMockImplementation: () => {
132+
throw new Error('not mocked');
133+
},
134+
}
135+
);
136+
137+
expect(() => mockObj.getSomethingWithArgs(1, 2)).toThrowError('not mocked');
138+
});
139+
127140
test('Can specify multiple calledWith', () => {
128141
const mockObj = mock<MockInt>();
129142
mockObj.getSomethingWithArgs.calledWith(1, 2).mockReturnValue(3);
@@ -225,6 +238,14 @@ describe('jest-mock-extended', () => {
225238
expect(mockObj.getSomethingWithArgs(7, 2)).toBe(undefined);
226239
});
227240

241+
test('supports overriding with same args', () => {
242+
const mockObj = mock<MockInt>();
243+
mockObj.getSomethingWithArgs.calledWith(1, 2).mockReturnValue(4);
244+
mockObj.getSomethingWithArgs.calledWith(1, 2).mockReturnValue(3);
245+
246+
expect(mockObj.getSomethingWithArgs(1, 2)).toBe(3);
247+
});
248+
228249
test('Support jest matcher', () => {
229250
const mockObj = mock<MockInt>();
230251
mockObj.getSomethingWithArgs.calledWith(expect.anything(), expect.anything()).mockReturnValue(3);
@@ -300,26 +321,58 @@ describe('jest-mock-extended', () => {
300321
mockObj.deepProp.getNumber(2);
301322
expect(mockObj.deepProp.getNumber).toHaveBeenCalledTimes(1);
302323
});
324+
325+
test('fallback mock implementation can be overridden', () => {
326+
const mockObj = mockDeep<Test1>({
327+
fallbackMockImplementation: () => {
328+
throw new Error('not mocked');
329+
},
330+
});
331+
mockObj.deepProp.getAnotherString.calledWith('foo'); // no mock implementation
332+
expect(() => mockObj.getNumber()).toThrowError('not mocked');
333+
expect(() => mockObj.deepProp.getAnotherString('foo')).toThrowError('not mocked');
334+
});
335+
336+
test('fallback mock implementation can be overridden while also providing a mock implementation', () => {
337+
const mockObj = mockDeep<Test1>(
338+
{
339+
fallbackMockImplementation: () => {
340+
throw new Error('not mocked');
341+
},
342+
},
343+
{
344+
getNumber: () => {
345+
return 150;
346+
},
347+
}
348+
);
349+
mockObj.deepProp.getAnotherString.calledWith('?').mockReturnValue('mocked');
350+
expect(mockObj.getNumber()).toBe(150);
351+
expect(mockObj.deepProp.getAnotherString('?')).toBe('mocked');
352+
353+
expect(() => mockObj.deepProp.getNumber(1)).toThrowError('not mocked');
354+
expect(() => mockObj.deepProp.getAnotherString('!')).toThrowError('not mocked');
355+
});
303356
});
304357

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

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

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

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

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

325378
expect(mockObj.funcValueProp.deepProp.getNumber.mock.calls[0][0]).toBe(100);

src/Mock.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,25 @@ export interface CalledWithMock<T, Y extends any[]> extends jest.Mock<T, Y> {
3131
}
3232

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

3838
export type DeepMockProxy<T> = {
39+
// This supports deep mocks in the else branch
40+
[K in keyof T]: T[K] extends (...args: infer A) => infer B ? CalledWithMock<B, A> : DeepMockProxy<T[K]>;
41+
} &
42+
T;
43+
44+
export type DeepMockProxyWithFuncPropSupport<T> = {
3945
// This supports deep mocks in the else branch
4046
[K in keyof T]: T[K] extends (...args: infer A) => infer B ? CalledWithMock<B, A> & DeepMockProxy<T[K]> : DeepMockProxy<T[K]>;
41-
} & T;
47+
} &
48+
T;
4249

4350
export interface MockOpts {
4451
deep?: boolean;
52+
fallbackMockImplementation?: (...args: any[]) => any;
4553
}
4654

4755
export const mockClear = (mock: MockProxy<any>) => {
@@ -87,7 +95,18 @@ export const mockReset = (mock: MockProxy<any>) => {
8795
}
8896
};
8997

90-
export const mockDeep = <T>(mockImplementation?: DeepPartial<T>) => mock<T, DeepMockProxy<T> & T>(mockImplementation, { deep: true });
98+
export function mockDeep<T>(
99+
opts: { funcPropSupport?: true; fallbackMockImplementation?: MockOpts['fallbackMockImplementation'] },
100+
mockImplementation?: DeepPartial<T>
101+
): DeepMockProxyWithFuncPropSupport<T>;
102+
export function mockDeep<T>(mockImplementation?: DeepPartial<T>): DeepMockProxy<T>;
103+
export function mockDeep(arg1: any, arg2?: any) {
104+
const [opts, mockImplementation] =
105+
typeof arg1 === 'object' && (typeof arg1.fallbackMockImplementation === 'function' || arg1.funcPropSupport === true)
106+
? [arg1, arg2]
107+
: [{}, arg1];
108+
return mock(mockImplementation, { deep: true, fallbackMockImplementation: opts.fallbackMockImplementation });
109+
}
91110

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

116135
get: (obj: MockProxy<any>, property: ProxiedProperty) => {
117-
let fn = calledWithFn();
136+
let fn = calledWithFn({ fallbackMockImplementation: opts?.fallbackMockImplementation });
118137

119138
// @ts-ignore
120139
if (!(property in obj)) {
@@ -137,7 +156,7 @@ const handler = (opts?: MockOpts) => ({
137156
obj[property]._isMockObject = true;
138157
} else {
139158
// @ts-ignore
140-
obj[property] = calledWithFn();
159+
obj[property] = calledWithFn({ fallbackMockImplementation: opts?.fallbackMockImplementation });
141160
}
142161
}
143162

0 commit comments

Comments
 (0)