Skip to content

Commit df69e8e

Browse files
Fix received optional parameter issue and better debuggability (#81)
* fix #59 * add substitute base class, exceptions and methods enum * replace harcoded values with custom centralized ones * update test regex * change SubstituteBase naming * remove duplicate key
1 parent edc0734 commit df69e8e

File tree

10 files changed

+186
-114
lines changed

10 files changed

+186
-114
lines changed

spec/issues/51.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@ test('issue 51 - All functions shares the same state', async t => {
1515
try {
1616
calculator.received().divide(1, 2);
1717
} catch (e) {
18-
t.regex(e.toString(), /Error: there is no mock for property: divide/);
18+
t.regex(e.toString(), /SubstituteException: There is no mock for property: divide/);
1919
}
2020
});

spec/issues/59.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ test('issue 59 - Mock function with optional parameters', (t) => {
1313
echoer.maybeEcho().returns('baz')
1414

1515
t.is(echoer.maybeEcho('foo'), 'bar')
16-
// echoer.received().maybeEcho('foo');
16+
echoer.received().maybeEcho('foo');
1717
t.is(echoer.maybeEcho(), 'baz')
1818
})

src/Context.ts

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1+
import { inspect } from 'util'
12
import { ContextState } from "./states/ContextState";
23
import { InitialState } from "./states/InitialState";
34
import { HandlerKey } from "./Substitute";
45
import { Type } from "./Utilities";
56
import { SetPropertyState } from "./states/SetPropertyState";
6-
7-
class SubstituteJS { }
7+
import { SubstituteJS as SubstituteBase, SubstituteException } from './SubstituteBase'
88

99
export class Context {
1010
private _initialState: InitialState;
@@ -19,24 +19,22 @@ export class Context {
1919

2020
constructor() {
2121
this._initialState = new InitialState();
22-
this._setState = this._initialState
22+
this._setState = this._initialState;
2323
this._getState = this._initialState;
2424

25-
this._proxy = new Proxy(SubstituteJS, {
25+
this._proxy = new Proxy(SubstituteBase, {
2626
apply: (_target, _this, args) => this.apply(_target, _this, args),
2727
set: (_target, property, value) => (this.set(_target, property, value), true),
28-
get: (_target, property) => this.get(_target, property),
29-
getOwnPropertyDescriptor: (obj, prop) => prop === 'constructor' ?
30-
{ value: obj, configurable: true } : Reflect.getOwnPropertyDescriptor(obj, prop)
28+
get: (_target, property) => this._filterAndReturnProperty(_target, property, this.get)
3129
});
3230

33-
this._rootProxy = new Proxy(SubstituteJS, {
31+
this._rootProxy = new Proxy(SubstituteBase, {
3432
apply: (_target, _this, args) => this.initialState.apply(this, args),
3533
set: (_target, property, value) => (this.initialState.set(this, property, value), true),
36-
get: (_target, property) => this.initialState.get(this, property)
34+
get: (_target, property) => this._filterAndReturnProperty(_target, property, this.rootGet)
3735
});
3836

39-
this._receivedProxy = new Proxy(SubstituteJS, {
37+
this._receivedProxy = new Proxy(SubstituteBase, {
4038
apply: (_target, _this, args) => this._receivedState === void 0 ? void 0 : this._receivedState.apply(this, args),
4139
set: (_target, property, value) => (this.set(_target, property, value), true),
4240
get: (_target, property) => {
@@ -50,12 +48,40 @@ export class Context {
5048
});
5149
}
5250

51+
private _filterAndReturnProperty(target: typeof SubstituteBase, property: PropertyKey, defaultGet: Context['get']) {
52+
switch (property) {
53+
case 'constructor':
54+
case 'valueOf':
55+
case '$$typeof':
56+
case 'length':
57+
case 'toString':
58+
case 'inspect':
59+
case 'lastRegisteredSubstituteJSMethodOrProperty':
60+
return target.prototype[property];
61+
case Symbol.toPrimitive:
62+
return target.prototype[Symbol.toPrimitive];
63+
case inspect.custom:
64+
return target.prototype[inspect.custom];
65+
case Symbol.iterator:
66+
return target.prototype[Symbol.iterator];
67+
case Symbol.toStringTag:
68+
return target.prototype[Symbol.toStringTag];
69+
default:
70+
target.prototype.lastRegisteredSubstituteJSMethodOrProperty = property.toString()
71+
return defaultGet.bind(this)(target, property);
72+
}
73+
}
74+
5375
private handleNotFoundState(property: PropertyKey) {
5476
if (this.initialState.hasExpectations && this.initialState.expectedCount !== null) {
5577
this.initialState.assertCallCountMatchesExpectations([], 0, Type.property, property, []);
5678
return this.receivedProxy;
5779
}
58-
throw new Error(`there is no mock for property: ${String(property)}`);
80+
throw SubstituteException.forPropertyNotMocked(property);
81+
}
82+
83+
rootGet(_target: any, property: PropertyKey) {
84+
return this.initialState.get(this, property);
5985
}
6086

6187
apply(_target: any, _this: any, args: any[]) {

src/SubstituteBase.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { inspect } from 'util';
2+
import { Type, stringifyArguments, stringifyCalls, Call } from './Utilities';
3+
4+
export class SubstituteJS {
5+
private _lastRegisteredSubstituteJSMethodOrProperty: string
6+
set lastRegisteredSubstituteJSMethodOrProperty(value: string) {
7+
this._lastRegisteredSubstituteJSMethodOrProperty = value;
8+
}
9+
get lastRegisteredSubstituteJSMethodOrProperty() {
10+
return typeof this._lastRegisteredSubstituteJSMethodOrProperty === 'undefined' ? 'root' : this._lastRegisteredSubstituteJSMethodOrProperty;
11+
}
12+
[Symbol.toPrimitive]() {
13+
return `[Function: ${this.constructor.name}] -> ${this.lastRegisteredSubstituteJSMethodOrProperty}`;
14+
}
15+
[Symbol.toStringTag]() {
16+
return `[Function: ${this.constructor.name}] -> ${this.lastRegisteredSubstituteJSMethodOrProperty}`;
17+
}
18+
[Symbol.iterator]() {
19+
return `[Function: ${this.constructor.name}] -> ${this.lastRegisteredSubstituteJSMethodOrProperty}`;
20+
}
21+
[inspect.custom]() {
22+
return `[Function: ${this.constructor.name}] -> ${this.lastRegisteredSubstituteJSMethodOrProperty}`;
23+
}
24+
valueOf() {
25+
return `[Function: ${this.constructor.name}] -> ${this.lastRegisteredSubstituteJSMethodOrProperty}`;
26+
}
27+
$$typeof() {
28+
return `[Function: ${this.constructor.name}] -> ${this.lastRegisteredSubstituteJSMethodOrProperty}`;
29+
}
30+
toString() {
31+
return `[Function: ${this.constructor.name}] -> ${this.lastRegisteredSubstituteJSMethodOrProperty}`;
32+
}
33+
inspect() {
34+
return `[Function: ${this.constructor.name}] -> ${this.lastRegisteredSubstituteJSMethodOrProperty}`;
35+
}
36+
length = `[Function: ${this.constructor.name}] -> ${this.lastRegisteredSubstituteJSMethodOrProperty}`;
37+
}
38+
39+
enum SubstituteExceptionTypes {
40+
CallCountMissMatch = 'CallCountMissMatch',
41+
PropertyNotMocked = 'PropertyNotMocked'
42+
}
43+
44+
export class SubstituteException extends Error {
45+
type: SubstituteExceptionTypes
46+
private constructor(msg: string, exceptionType?: SubstituteExceptionTypes) {
47+
super(msg);
48+
Error.captureStackTrace(this, SubstituteException);
49+
this.name = new.target.name;
50+
this.type = exceptionType
51+
}
52+
53+
static forCallCountMissMatch(
54+
callCount: { expected: number | null, received: number },
55+
property: { type: Type, value: PropertyKey },
56+
calls: { expectedArguments: any[], received: Call[] }
57+
) {
58+
const message = 'Expected ' + (callCount.expected === null ? '1 or more' : callCount.expected) +
59+
' call' + (callCount.expected === 1 ? '' : 's') + ' to the ' + property.type + ' ' + property.value.toString() +
60+
' with ' + stringifyArguments(calls.expectedArguments) + ', but received ' + (callCount.received === 0 ? 'none' : callCount.received) +
61+
' of such call' + (callCount.received === 1 ? '' : 's') +
62+
'.\nAll calls received to ' + property.type + ' ' + property.value.toString() + ':' + stringifyCalls(calls.received);
63+
return new this(message, SubstituteExceptionTypes.CallCountMissMatch);
64+
}
65+
66+
static forPropertyNotMocked(property: PropertyKey) {
67+
return new this(`There is no mock for property: ${String(property)}`, SubstituteExceptionTypes.PropertyNotMocked)
68+
}
69+
70+
static generic(message: string) {
71+
return new this(message)
72+
}
73+
}

src/Transformations.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ export type ObjectSubstitute<T extends Object, K extends Object = T> = ObjectSub
3030

3131
type TerminatingObject<T> = {
3232
[P in keyof T]:
33-
T[P] extends () => infer R ? () => void :
34-
T[P] extends (...args: infer F) => infer R ? (...args: F) => void :
33+
T[P] extends (...args: infer F) => any ? (...args: F) => void :
34+
T[P] extends () => any ? () => void :
3535
T[P];
3636
}
3737

src/Utilities.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Argument, AllArguments } from "./Arguments";
2-
import { GetPropertyState } from './states/GetPropertyState'
3-
import { InitialState } from './states/InitialState'
4-
import { Context } from './Context'
5-
import util = require('util')
2+
import { GetPropertyState } from './states/GetPropertyState';
3+
import { InitialState } from './states/InitialState';
4+
import { Context } from './Context';
5+
import * as util from 'util';
66

77
export type Call = any[] // list of args
88

@@ -11,6 +11,14 @@ export enum Type {
1111
property = 'property'
1212
}
1313

14+
export enum SubstituteMethods {
15+
received = 'received',
16+
didNotReceive = 'didNotReceive',
17+
mimicks = 'mimicks',
18+
throws = 'throws',
19+
returns = 'returns'
20+
}
21+
1422
export const Nothing = Symbol();
1523
export type Nothing = typeof Nothing
1624

src/states/FunctionState.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { ContextState, PropertyKey } from "./ContextState";
22
import { Context } from "src/Context";
3-
import { areArgumentArraysEqual, Call, Type } from "../Utilities";
3+
import { SubstituteMethods, areArgumentArraysEqual, Call, Type } from "../Utilities";
44
import { GetPropertyState } from "./GetPropertyState";
5+
import { SubstituteException } from "../SubstituteBase";
56

67
interface ReturnMock {
78
args: Call
@@ -100,10 +101,10 @@ export class FunctionState implements ContextState {
100101
if (property === 'then')
101102
return void 0;
102103

103-
if (property === 'mimicks') {
104+
if (property === SubstituteMethods.mimicks) {
104105
return (input: Function) => {
105106
if (!this._lastArgs) {
106-
throw new Error('Eh, there\'s a bug, no args recorded for this mimicks :/')
107+
throw SubstituteException.generic('Eh, there\'s a bug, no args recorded for this mimicks :/')
107108
}
108109
this.mimicks.push({
109110
args: this._lastArgs,
@@ -115,10 +116,10 @@ export class FunctionState implements ContextState {
115116
}
116117
}
117118

118-
if (property === 'throws') {
119+
if (property === SubstituteMethods.throws) {
119120
return (input: Error | Function) => {
120121
if (!this._lastArgs) {
121-
throw new Error('Eh, there\'s a bug, no args recorded for this throw :/')
122+
throw SubstituteException.generic('Eh, there\'s a bug, no args recorded for this throw :/')
122123
}
123124
this.throws.push({
124125
args: this._lastArgs,
@@ -129,10 +130,10 @@ export class FunctionState implements ContextState {
129130
}
130131
}
131132

132-
if (property === 'returns') {
133+
if (property === SubstituteMethods.returns) {
133134
return (...returns: any[]) => {
134135
if (!this._lastArgs) {
135-
throw new Error('Eh, there\'s a bug, no args recorded for this return :/')
136+
throw SubstituteException.generic('Eh, there\'s a bug, no args recorded for this return :/')
136137
}
137138
this.returns.push({
138139
returnValues: returns,

src/states/GetPropertyState.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { ContextState, PropertyKey } from "./ContextState";
22
import { Context } from "src/Context";
33
import { FunctionState } from "./FunctionState";
4-
import { Type, Nothing } from "../Utilities";
4+
import { Type, Nothing, SubstituteMethods } from "../Utilities";
5+
import { SubstituteException } from "../SubstituteBase";
56

67
export class GetPropertyState implements ContextState {
78
private returns: any[] | Nothing;
@@ -49,8 +50,7 @@ export class GetPropertyState implements ContextState {
4950
return context.apply(void 0, void 0, args);
5051
}
5152

52-
set(context: Context, property: PropertyKey, value: any) {
53-
}
53+
set(context: Context, property: PropertyKey, value: any) { }
5454

5555
get(context: Context, property: PropertyKey) {
5656
const hasExpectations = context.initialState.hasExpectations;
@@ -61,7 +61,7 @@ export class GetPropertyState implements ContextState {
6161
if (this.isFunction)
6262
return context.proxy;
6363

64-
if (property === 'mimicks') {
64+
if (property === SubstituteMethods.mimicks) {
6565
return (input: Function) => {
6666
this.mimicks = input;
6767
this._callCount--;
@@ -70,9 +70,9 @@ export class GetPropertyState implements ContextState {
7070
}
7171
}
7272

73-
if (property === 'returns') {
73+
if (property === SubstituteMethods.returns) {
7474
if (this.returns !== Nothing)
75-
throw new Error('The return value for the property ' + this._property.toString() + ' has already been set to ' + this.returns);
75+
throw SubstituteException.generic('The return value for the property ' + this._property.toString() + ' has already been set to ' + this.returns);
7676

7777
return (...returns: any[]) => {
7878
this.returns = returns;
@@ -82,7 +82,7 @@ export class GetPropertyState implements ContextState {
8282
};
8383
}
8484

85-
if (property === 'throws') {
85+
if (property === SubstituteMethods.throws) {
8686
return (callback: Function) => {
8787
this.throws = callback;
8888
this._callCount--;
@@ -92,22 +92,22 @@ export class GetPropertyState implements ContextState {
9292
}
9393

9494
if (!hasExpectations) {
95-
this._callCount++;
95+
this._callCount++;
96+
97+
if (this.mimicks !== Nothing)
98+
return this.mimicks.apply(this.mimicks);
9699

97-
if (this.mimicks !== Nothing)
98-
return this.mimicks.apply(this.mimicks);
99-
100-
if (this.throws !== Nothing)
101-
throw this.throws
100+
if (this.throws !== Nothing)
101+
throw this.throws
102102

103-
if (this.returns !== Nothing) {
104-
var returnsArray = this.returns as any[];
105-
if (returnsArray.length === 1)
106-
return returnsArray[0];
103+
if (this.returns !== Nothing) {
104+
var returnsArray = this.returns as any[];
105+
if (returnsArray.length === 1)
106+
return returnsArray[0];
107107

108-
return returnsArray[this._callCount - 1];
109-
}
108+
return returnsArray[this._callCount - 1];
110109
}
110+
}
111111

112112
context.initialState.assertCallCountMatchesExpectations(
113113
[[]], // I'm not sure what this was supposed to mean

0 commit comments

Comments
 (0)