or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

async-mocking.mdcore-mocking.mdhttp-mocking.mdindex.mdspy-functions.mdsync-mocking.mdsystem-mocking.md

spy-functions.mddocs/

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

```