0
# Spy Functionality
1
2
Track function calls without changing their behavior. Spy functions monitor call counts, arguments, and timing while preserving the original function's implementation and return values.
3
4
## Capabilities
5
6
### Basic Spy Function
7
8
Create a spy wrapper around an existing function to track calls without modifying behavior.
9
10
```typescript { .api }
11
/**
12
* Create a spy wrapper around existing function without changing behavior
13
* @param mod - Object containing the method to spy on
14
* @param method - Method name to spy on
15
* @throws Error if target is not a function
16
*/
17
function spy(mod: any, method: string | symbol): void;
18
```
19
20
**Usage Examples:**
21
22
```typescript
23
import { spy } from "mm";
24
25
const calculator = {
26
add: (a: number, b: number) => a + b,
27
multiply: async (a: number, b: number) => a * b
28
};
29
30
// Spy on synchronous function
31
spy(calculator, "add");
32
const result1 = calculator.add(2, 3); // => 5 (original behavior)
33
console.log(calculator.add.called); // => 1
34
console.log(calculator.add.lastCalledArguments); // => [2, 3]
35
36
// Spy on async function
37
spy(calculator, "multiply");
38
const result2 = await calculator.multiply(4, 5); // => 20 (original behavior)
39
console.log(calculator.multiply.called); // => 1
40
console.log(calculator.multiply.lastCalledArguments); // => [4, 5]
41
```
42
43
### Class Method Mocking
44
45
Mock methods on class prototypes, affecting all instances of the class.
46
47
```typescript { .api }
48
/**
49
* Mock method on class prototype affecting all instances
50
* @param instance - Any instance of the class to mock
51
* @param property - Method name to mock on the prototype
52
* @param value - Replacement method or value (optional)
53
*/
54
function classMethod(instance: any, property: PropertyKey, value?: any): void;
55
```
56
57
**Usage Examples:**
58
59
```typescript
60
import { classMethod } from "mm";
61
62
class UserService {
63
async fetchUser(id: string) {
64
// Real database call
65
return { id, name: "Real User", email: "real@example.com" };
66
}
67
}
68
69
const service1 = new UserService();
70
const service2 = new UserService();
71
72
// Mock affects all instances
73
classMethod(service1, "fetchUser", async (id: string) => ({
74
id,
75
name: "Mock User",
76
email: "mock@example.com"
77
}));
78
79
// Both instances now use the mock
80
const user1 = await service1.fetchUser("123"); // Mock data
81
const user2 = await service2.fetchUser("456"); // Mock data
82
83
console.log(service1.fetchUser.called); // => 1
84
console.log(service2.fetchUser.called); // => 1 (separate spy tracking)
85
```
86
87
### Automatic Spy Properties
88
89
All mocked functions (including spies) automatically receive tracking properties:
90
91
```typescript { .api }
92
interface SpyProperties {
93
/** Number of times the function has been called */
94
called: number;
95
/** Array containing arguments from each function call */
96
calledArguments: any[][];
97
/** Arguments from the most recent function call */
98
lastCalledArguments: any[];
99
}
100
```
101
102
**Usage Examples:**
103
104
```typescript
105
import { spy } from "mm";
106
107
const api = {
108
request: async (method: string, url: string, data?: any) => {
109
// Real API implementation
110
return { status: 200, data: "real response" };
111
}
112
};
113
114
spy(api, "request");
115
116
// Make several calls
117
await api.request("GET", "/users");
118
await api.request("POST", "/users", { name: "Alice" });
119
await api.request("PUT", "/users/1", { name: "Bob" });
120
121
// Check call tracking
122
console.log(api.request.called); // => 3
123
124
console.log(api.request.calledArguments);
125
// => [
126
// ["GET", "/users"],
127
// ["POST", "/users", { name: "Alice" }],
128
// ["PUT", "/users/1", { name: "Bob" }]
129
// ]
130
131
console.log(api.request.lastCalledArguments);
132
// => ["PUT", "/users/1", { name: "Bob" }]
133
```
134
135
### Spy Validation
136
137
The spy function validates that the target is actually a function:
138
139
```typescript
140
import { spy } from "mm";
141
142
const obj = {
143
value: 42,
144
calculate: (x: number) => x * 2
145
};
146
147
// This works - calculate is a function
148
spy(obj, "calculate");
149
150
// This throws an error - value is not a function
151
try {
152
spy(obj, "value");
153
} catch (err) {
154
console.log(err.message); // => "spy target value is not a function"
155
}
156
```
157
158
## Spy vs Mock Differences
159
160
### Pure Spy (Original Behavior)
161
162
Using `spy()` preserves the original function behavior:
163
164
```typescript
165
import { spy } from "mm";
166
167
const math = {
168
square: (n: number) => n * n
169
};
170
171
spy(math, "square");
172
const result = math.square(5); // => 25 (real calculation)
173
console.log(math.square.called); // => 1
174
```
175
176
### Spy with Mock (Changed Behavior)
177
178
Using `mock()` changes behavior but still adds spy properties:
179
180
```typescript
181
import { mock } from "mm";
182
183
const math = {
184
square: (n: number) => n * n
185
};
186
187
mock(math, "square", (n: number) => 999); // Always return 999
188
const result = math.square(5); // => 999 (mocked behavior)
189
console.log(math.square.called); // => 1
190
```
191
192
## Advanced Spy Patterns
193
194
### Conditional Spying
195
196
Spy on functions conditionally based on test requirements:
197
198
```typescript
199
import { spy } from "mm";
200
201
const logger = {
202
debug: (msg: string) => console.log(`[DEBUG] ${msg}`),
203
info: (msg: string) => console.log(`[INFO] ${msg}`),
204
error: (msg: string) => console.error(`[ERROR] ${msg}`)
205
};
206
207
// Spy on all logger methods
208
Object.keys(logger).forEach(method => {
209
if (typeof logger[method] === "function") {
210
spy(logger, method);
211
}
212
});
213
214
logger.info("Test message");
215
logger.error("Test error");
216
217
console.log(logger.info.called); // => 1
218
console.log(logger.error.called); // => 1
219
console.log(logger.debug.called); // => 0
220
```
221
222
### Method Chain Spying
223
224
Spy on method chains to track call sequences:
225
226
```typescript
227
import { spy } from "mm";
228
229
const queryBuilder = {
230
select: function(fields: string) {
231
console.log(`SELECT ${fields}`);
232
return this;
233
},
234
where: function(condition: string) {
235
console.log(`WHERE ${condition}`);
236
return this;
237
},
238
execute: function() {
239
console.log("EXECUTE");
240
return "query results";
241
}
242
};
243
244
// Spy on all methods
245
spy(queryBuilder, "select");
246
spy(queryBuilder, "where");
247
spy(queryBuilder, "execute");
248
249
// Use the chain
250
const results = queryBuilder
251
.select("name, email")
252
.where("age > 18")
253
.execute();
254
255
console.log(queryBuilder.select.called); // => 1
256
console.log(queryBuilder.where.called); // => 1
257
console.log(queryBuilder.execute.called); // => 1
258
```
259
260
### Class Instance Tracking
261
262
Track method calls across different class instances:
263
264
```typescript
265
import { spy } from "mm";
266
267
class DatabaseConnection {
268
connect() {
269
return "connected";
270
}
271
272
query(sql: string) {
273
return `results for: ${sql}`;
274
}
275
}
276
277
const db1 = new DatabaseConnection();
278
const db2 = new DatabaseConnection();
279
280
// Spy on instances separately
281
spy(db1, "query");
282
spy(db2, "query");
283
284
db1.query("SELECT * FROM users");
285
db2.query("SELECT * FROM posts");
286
db1.query("SELECT * FROM orders");
287
288
console.log(db1.query.called); // => 2
289
console.log(db2.query.called); // => 1
290
```
291
292
## Integration with Testing Frameworks
293
294
Spy functionality integrates well with testing frameworks:
295
296
```typescript
297
import { spy, restore } from "mm";
298
import { expect } from "chai";
299
300
describe("User Service", () => {
301
afterEach(() => {
302
restore(); // Clean up spies after each test
303
});
304
305
it("should call database twice for user details", async () => {
306
const db = {
307
query: async (sql: string) => [{ id: 1, name: "User" }]
308
};
309
310
const userService = new UserService(db);
311
spy(db, "query");
312
313
await userService.getUserWithProfile(1);
314
315
expect(db.query.called).to.equal(2);
316
expect(db.query.calledArguments[0][0]).to.include("SELECT * FROM users");
317
expect(db.query.calledArguments[1][0]).to.include("SELECT * FROM profiles");
318
});
319
});
320
```
321
322
## Types
323
324
```typescript { .api }
325
// Spy properties added to all functions
326
interface SpyProperties {
327
called: number;
328
calledArguments: any[][];
329
lastCalledArguments: any[];
330
}
331
332
// Property key types for spying
333
type PropertyKey = string | number | symbol;
334
```