@martinffx/atelier 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/README.md +285 -0
- package/agents/architect.md +38 -0
- package/agents/oracle.md +40 -0
- package/agents/recon.md +39 -0
- package/dist/atelier.js +33082 -0
- package/package.json +44 -0
- package/skills/code-conventional-commit/SKILL.md +134 -0
- package/skills/code-conventional-commit/evals/evals.json +23 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-0/with_skill/outputs/commit-message.txt +4 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-0/with_skill/outputs/eval_metadata.json +6 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-1/with_skill/outputs/eval_metadata.json +6 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-1/with_skill/outputs/validation-result.txt +16 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-2/with_skill/outputs/commit-message.txt +8 -0
- package/skills/code-conventional-commit/workspace/iteration-1/eval-2/with_skill/outputs/eval_metadata.json +6 -0
- package/skills/code-conventional-commit/workspace/iteration-1/report.html +1325 -0
- package/skills/code-debug/SKILL.md +271 -0
- package/skills/code-debug/references/language-tools.md +139 -0
- package/skills/code-debug/references/techniques.md +117 -0
- package/skills/code-docs/SKILL.md +171 -0
- package/skills/code-docs/references/api-docs.md +116 -0
- package/skills/code-docs/references/changelog.md +64 -0
- package/skills/code-docs/references/readme-structure.md +94 -0
- package/skills/code-git-worktrees/SKILL.md +218 -0
- package/skills/code-perf/SKILL.md +163 -0
- package/skills/code-perf/references/common-bottlenecks.md +124 -0
- package/skills/code-perf/references/optimization-patterns.md +167 -0
- package/skills/code-perf/references/profiling-tools.md +126 -0
- package/skills/code-review/SKILL.md +78 -0
- package/skills/code-review/references/output.md +200 -0
- package/skills/code-review/references/reviewers.md +719 -0
- package/skills/code-review/references/rq.md +357 -0
- package/skills/code-review/references/rs.md +30 -0
- package/skills/code-security/SKILL.md +183 -0
- package/skills/code-security/references/owasp-top-10.md +129 -0
- package/skills/code-security/references/security-tools.md +102 -0
- package/skills/code-security/references/vulnerability-patterns.md +129 -0
- package/skills/code-stacked-commit/SKILL.md +155 -0
- package/skills/code-subagents/SKILL.md +184 -0
- package/skills/code-subagents/references/code-quality-reviewer-prompt.md +48 -0
- package/skills/code-subagents/references/implementor-prompt.md +79 -0
- package/skills/code-subagents/references/spec-reviewer-prompt.md +62 -0
- package/skills/oracle-architect/SKILL.md +246 -0
- package/skills/oracle-architect/references/api-design.md +345 -0
- package/skills/oracle-architect/references/data-modeling.md +318 -0
- package/skills/oracle-architect/references/ddd-patterns.md +247 -0
- package/skills/oracle-challenge/SKILL.md +126 -0
- package/skills/oracle-testing/SKILL.md +237 -0
- package/skills/oracle-testing/references/anti-patterns.md +618 -0
- package/skills/oracle-testing/references/boundaries.md +579 -0
- package/skills/oracle-testing/references/core-testing.md +691 -0
- package/skills/oracle-testing/references/edge-testing.md +794 -0
- package/skills/oracle-testing/references/stub-driven-tdd.md +322 -0
- package/skills/oracle-thinkdeep/SKILL.md +164 -0
- package/skills/python-architecture/SKILL.md +148 -0
- package/skills/python-architecture/references/data-modeling.md +558 -0
- package/skills/python-architecture/references/ddd.md +569 -0
- package/skills/python-architecture/references/functional-core.md +337 -0
- package/skills/python-build-tools/SKILL.md +153 -0
- package/skills/python-build-tools/references/basedpyright.md +321 -0
- package/skills/python-build-tools/references/ruff.md +225 -0
- package/skills/python-build-tools/references/uv.md +257 -0
- package/skills/python-fastapi/SKILL.md +184 -0
- package/skills/python-fastapi/references/api-design.md +563 -0
- package/skills/python-fastapi/references/dependencies.md +479 -0
- package/skills/python-fastapi/references/middleware.md +475 -0
- package/skills/python-fastapi/references/validation.md +497 -0
- package/skills/python-modern-python/SKILL.md +201 -0
- package/skills/python-modern-python/references/async.md +76 -0
- package/skills/python-modern-python/references/pattern-matching.md +89 -0
- package/skills/python-modern-python/references/typing.md +96 -0
- package/skills/python-monorepo/SKILL.md +146 -0
- package/skills/python-monorepo/references/docker.md +515 -0
- package/skills/python-monorepo/references/namespace-packages.md +426 -0
- package/skills/python-monorepo/references/workspace-config.md +341 -0
- package/skills/python-sqlalchemy/SKILL.md +119 -0
- package/skills/python-sqlalchemy/references/async.md +51 -0
- package/skills/python-sqlalchemy/references/models.md +67 -0
- package/skills/python-sqlalchemy/references/queries.md +57 -0
- package/skills/python-temporal/SKILL.md +128 -0
- package/skills/python-temporal/references/patterns.md +87 -0
- package/skills/python-temporal/references/testing.md +54 -0
- package/skills/python-testing/SKILL.md +161 -0
- package/skills/python-testing/references/boundaries.md +449 -0
- package/skills/python-testing/references/mocking.md +449 -0
- package/skills/python-testing/references/pytest.md +524 -0
- package/skills/spec-brainstorm/SKILL.md +325 -0
- package/skills/spec-finish/SKILL.md +159 -0
- package/skills/spec-implement/SKILL.md +296 -0
- package/skills/spec-orchestrator/SKILL.md +156 -0
- package/skills/spec-plan/SKILL.md +248 -0
- package/skills/typescript-api-design/SKILL.md +396 -0
- package/skills/typescript-api-design/references/error-responses.md +469 -0
- package/skills/typescript-build-tools/SKILL.md +447 -0
- package/skills/typescript-build-tools/references/biome.md +436 -0
- package/skills/typescript-build-tools/references/bun.md +432 -0
- package/skills/typescript-build-tools/references/turborepo.md +426 -0
- package/skills/typescript-build-tools/references/vitest.md +518 -0
- package/skills/typescript-drizzle-orm/SKILL.md +398 -0
- package/skills/typescript-drizzle-orm/references/cloudflare.md +392 -0
- package/skills/typescript-drizzle-orm/references/entity-pattern.md +412 -0
- package/skills/typescript-drizzle-orm/references/postgresql.md +191 -0
- package/skills/typescript-drizzle-orm/references/repository-pattern.md +479 -0
- package/skills/typescript-drizzle-orm/references/sqlite.md +291 -0
- package/skills/typescript-dynamodb-toolbox/SKILL.md +370 -0
- package/skills/typescript-dynamodb-toolbox/references/entity-layer.md +574 -0
- package/skills/typescript-dynamodb-toolbox/references/error-handling.md +495 -0
- package/skills/typescript-dynamodb-toolbox/references/modeling.md +332 -0
- package/skills/typescript-dynamodb-toolbox/references/repository-layer.md +631 -0
- package/skills/typescript-dynamodb-toolbox/references/testing.md +718 -0
- package/skills/typescript-dynamodb-toolbox/references/transactions.md +423 -0
- package/skills/typescript-effect-ts/SKILL.md +341 -0
- package/skills/typescript-effect-ts/references/core.md +412 -0
- package/skills/typescript-effect-ts/references/error-handling.md +468 -0
- package/skills/typescript-effect-ts/references/resources.md +476 -0
- package/skills/typescript-effect-ts/references/schema.md +536 -0
- package/skills/typescript-effect-ts/references/services.md +461 -0
- package/skills/typescript-fastify/SKILL.md +405 -0
- package/skills/typescript-fastify/references/plugins.md +329 -0
- package/skills/typescript-fastify/references/typeid.md +315 -0
- package/skills/typescript-functional-patterns/SKILL.md +402 -0
- package/skills/typescript-functional-patterns/references/adts.md +497 -0
- package/skills/typescript-functional-patterns/references/branded-types.md +542 -0
- package/skills/typescript-functional-patterns/references/migration-guide.md +695 -0
- package/skills/typescript-functional-patterns/references/option-result.md +585 -0
- package/skills/typescript-testing/SKILL.md +353 -0
- package/skills/typescript-testing/references/mocking.md +675 -0
- package/skills/typescript-testing/references/msw.md +852 -0
- package/skills/typescript-testing/references/snapshot-testing.md +774 -0
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
# Mocking Patterns with Vitest
|
|
2
|
+
|
|
3
|
+
Comprehensive guide to mocking modules, functions, classes, and dependencies in Vitest tests.
|
|
4
|
+
|
|
5
|
+
## Module Mocking
|
|
6
|
+
|
|
7
|
+
### Auto-mocking Entire Modules
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { vi } from 'vitest'
|
|
11
|
+
import * as userRepo from './user-repository'
|
|
12
|
+
|
|
13
|
+
// Auto-mock all exports
|
|
14
|
+
vi.mock('./user-repository')
|
|
15
|
+
|
|
16
|
+
// All functions are now mocks
|
|
17
|
+
expect(vi.isMockFunction(userRepo.findById)).toBe(true)
|
|
18
|
+
expect(vi.isMockFunction(userRepo.save)).toBe(true)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Partial Module Mocking
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
vi.mock('./user-repository', () => ({
|
|
25
|
+
// Keep real implementation
|
|
26
|
+
...vi.importActual('./user-repository'),
|
|
27
|
+
|
|
28
|
+
// Mock specific functions
|
|
29
|
+
findById: vi.fn(),
|
|
30
|
+
save: vi.fn(),
|
|
31
|
+
}))
|
|
32
|
+
|
|
33
|
+
// Now some functions are real, some are mocked
|
|
34
|
+
await userRepo.validateEmail('test@example.com') // real
|
|
35
|
+
await userRepo.findById('123') // mocked
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Factory Functions
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
vi.mock('./database', () => {
|
|
42
|
+
const mockDb = {
|
|
43
|
+
connect: vi.fn().mockResolvedValue(undefined),
|
|
44
|
+
disconnect: vi.fn().mockResolvedValue(undefined),
|
|
45
|
+
query: vi.fn(),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
createConnection: () => mockDb,
|
|
50
|
+
Database: class MockDatabase {
|
|
51
|
+
connect = mockDb.connect
|
|
52
|
+
query = mockDb.query
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Typed Mocks
|
|
59
|
+
|
|
60
|
+
### Creating Typed Mock Objects
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import { vi } from 'vitest'
|
|
64
|
+
import type { UserRepository } from './types'
|
|
65
|
+
|
|
66
|
+
// Create fully typed mock
|
|
67
|
+
const mockUserRepo = vi.mocked<UserRepository>({
|
|
68
|
+
findById: vi.fn(),
|
|
69
|
+
findByEmail: vi.fn(),
|
|
70
|
+
save: vi.fn(),
|
|
71
|
+
delete: vi.fn(),
|
|
72
|
+
list: vi.fn(),
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// Type-safe mock setup
|
|
76
|
+
mockUserRepo.findById.mockResolvedValue({
|
|
77
|
+
id: '123',
|
|
78
|
+
name: 'Alice',
|
|
79
|
+
email: 'alice@example.com',
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// TypeScript catches type errors
|
|
83
|
+
// @ts-expect-error - wrong return type
|
|
84
|
+
mockUserRepo.findById.mockResolvedValue({ invalid: 'data' })
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Generic Typed Mocks
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
function createMockRepository<T>(): Repository<T> {
|
|
91
|
+
return {
|
|
92
|
+
findById: vi.fn(),
|
|
93
|
+
save: vi.fn(),
|
|
94
|
+
delete: vi.fn(),
|
|
95
|
+
list: vi.fn(),
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Use with specific types
|
|
100
|
+
const userRepo = createMockRepository<User>()
|
|
101
|
+
const orderRepo = createMockRepository<Order>()
|
|
102
|
+
|
|
103
|
+
// Fully typed
|
|
104
|
+
userRepo.findById.mockResolvedValue(user) // User type
|
|
105
|
+
orderRepo.findById.mockResolvedValue(order) // Order type
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Mock Implementations
|
|
109
|
+
|
|
110
|
+
### Return Values
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// Single return value
|
|
114
|
+
mockFn.mockReturnValue(42)
|
|
115
|
+
mockFn.mockResolvedValue(user) // async
|
|
116
|
+
|
|
117
|
+
// One-time return values
|
|
118
|
+
mockFn
|
|
119
|
+
.mockReturnValueOnce(1)
|
|
120
|
+
.mockReturnValueOnce(2)
|
|
121
|
+
.mockReturnValue(3) // default for subsequent calls
|
|
122
|
+
|
|
123
|
+
// Async one-time values
|
|
124
|
+
mockFn
|
|
125
|
+
.mockResolvedValueOnce(user1)
|
|
126
|
+
.mockResolvedValueOnce(user2)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Custom Implementations
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// Simple logic
|
|
133
|
+
mockFn.mockImplementation((x) => x * 2)
|
|
134
|
+
|
|
135
|
+
// Async logic
|
|
136
|
+
mockFn.mockImplementation(async (id) => {
|
|
137
|
+
if (id === '123') return user
|
|
138
|
+
throw new Error('Not found')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// Stateful logic
|
|
142
|
+
let callCount = 0
|
|
143
|
+
mockFn.mockImplementation(() => {
|
|
144
|
+
callCount++
|
|
145
|
+
if (callCount === 1) return 'first'
|
|
146
|
+
if (callCount === 2) return 'second'
|
|
147
|
+
return 'default'
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
// One-time implementation
|
|
151
|
+
mockFn.mockImplementationOnce(() => 'once')
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Error Throwing
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// Synchronous errors
|
|
158
|
+
mockFn.mockImplementation(() => {
|
|
159
|
+
throw new Error('Something went wrong')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// Async errors (rejected promises)
|
|
163
|
+
mockFn.mockRejectedValue(new Error('Database error'))
|
|
164
|
+
|
|
165
|
+
// One-time errors
|
|
166
|
+
mockFn
|
|
167
|
+
.mockRejectedValueOnce(new Error('First call fails'))
|
|
168
|
+
.mockResolvedValue(user) // subsequent calls succeed
|
|
169
|
+
|
|
170
|
+
// Conditional errors
|
|
171
|
+
mockFn.mockImplementation(async (id) => {
|
|
172
|
+
if (id === 'invalid') {
|
|
173
|
+
throw new Error('Invalid ID')
|
|
174
|
+
}
|
|
175
|
+
return data
|
|
176
|
+
})
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Spies
|
|
180
|
+
|
|
181
|
+
### Spying on Existing Functions
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import { vi } from 'vitest'
|
|
185
|
+
import { emailService } from './email-service'
|
|
186
|
+
|
|
187
|
+
// Spy without replacing implementation
|
|
188
|
+
const sendSpy = vi.spyOn(emailService, 'send')
|
|
189
|
+
|
|
190
|
+
// Function still works normally
|
|
191
|
+
await emailService.send({ to: 'alice@example.com', subject: 'Test' })
|
|
192
|
+
|
|
193
|
+
// But you can assert on calls
|
|
194
|
+
expect(sendSpy).toHaveBeenCalledOnce()
|
|
195
|
+
expect(sendSpy).toHaveBeenCalledWith({
|
|
196
|
+
to: 'alice@example.com',
|
|
197
|
+
subject: 'Test',
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
// Temporarily override
|
|
201
|
+
sendSpy.mockResolvedValueOnce({ messageId: 'mock-123' })
|
|
202
|
+
await emailService.send({ to: 'bob@example.com', subject: 'Test' })
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Spying on Object Methods
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
const user = {
|
|
209
|
+
name: 'Alice',
|
|
210
|
+
greet() {
|
|
211
|
+
return `Hello, ${this.name}`
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const greetSpy = vi.spyOn(user, 'greet')
|
|
216
|
+
|
|
217
|
+
// Call real method
|
|
218
|
+
expect(user.greet()).toBe('Hello, Alice')
|
|
219
|
+
|
|
220
|
+
// Assert it was called
|
|
221
|
+
expect(greetSpy).toHaveBeenCalled()
|
|
222
|
+
|
|
223
|
+
// Override for specific test
|
|
224
|
+
greetSpy.mockReturnValue('Mock greeting')
|
|
225
|
+
expect(user.greet()).toBe('Mock greeting')
|
|
226
|
+
|
|
227
|
+
// Restore original
|
|
228
|
+
greetSpy.mockRestore()
|
|
229
|
+
expect(user.greet()).toBe('Hello, Alice')
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Spying on Constructors
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
class Database {
|
|
236
|
+
constructor(public url: string) {}
|
|
237
|
+
async connect() { /* ... */ }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const DatabaseSpy = vi.spyOn(Database.prototype, 'connect')
|
|
241
|
+
|
|
242
|
+
const db = new Database('postgres://localhost')
|
|
243
|
+
await db.connect()
|
|
244
|
+
|
|
245
|
+
expect(DatabaseSpy).toHaveBeenCalledOnce()
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Class Mocking
|
|
249
|
+
|
|
250
|
+
### Mocking Entire Classes
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
vi.mock('./database', () => {
|
|
254
|
+
return {
|
|
255
|
+
Database: vi.fn().mockImplementation(() => ({
|
|
256
|
+
connect: vi.fn().mockResolvedValue(undefined),
|
|
257
|
+
query: vi.fn().mockResolvedValue([]),
|
|
258
|
+
disconnect: vi.fn().mockResolvedValue(undefined),
|
|
259
|
+
}))
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
import { Database } from './database'
|
|
264
|
+
|
|
265
|
+
// Database is now a mock constructor
|
|
266
|
+
const db = new Database('url')
|
|
267
|
+
await db.connect()
|
|
268
|
+
|
|
269
|
+
expect(Database).toHaveBeenCalledWith('url')
|
|
270
|
+
expect(db.connect).toHaveBeenCalled()
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Mocking with Class Instances
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
class MockDatabase {
|
|
277
|
+
connect = vi.fn().mockResolvedValue(undefined)
|
|
278
|
+
query = vi.fn().mockResolvedValue([])
|
|
279
|
+
disconnect = vi.fn().mockResolvedValue(undefined)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
vi.mock('./database', () => ({
|
|
283
|
+
Database: MockDatabase
|
|
284
|
+
}))
|
|
285
|
+
|
|
286
|
+
// Use in tests
|
|
287
|
+
const db = new Database()
|
|
288
|
+
await db.connect()
|
|
289
|
+
|
|
290
|
+
expect(db.connect).toHaveBeenCalled()
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Dependency Injection Patterns
|
|
294
|
+
|
|
295
|
+
### Constructor Injection
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
class UserService {
|
|
299
|
+
constructor(
|
|
300
|
+
private repo: UserRepository,
|
|
301
|
+
private email: EmailService,
|
|
302
|
+
) {}
|
|
303
|
+
|
|
304
|
+
async createUser(data: CreateUserInput) {
|
|
305
|
+
const user = await this.repo.save(data)
|
|
306
|
+
await this.email.sendWelcome(user.email)
|
|
307
|
+
return user
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Test with mocked dependencies
|
|
312
|
+
it('creates user and sends email', async () => {
|
|
313
|
+
const mockRepo = vi.mocked<UserRepository>({
|
|
314
|
+
save: vi.fn().mockResolvedValue(user)
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
const mockEmail = vi.mocked<EmailService>({
|
|
318
|
+
sendWelcome: vi.fn().mockResolvedValue(undefined)
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
const service = new UserService(mockRepo, mockEmail)
|
|
322
|
+
const result = await service.createUser(userData)
|
|
323
|
+
|
|
324
|
+
expect(mockRepo.save).toHaveBeenCalledWith(userData)
|
|
325
|
+
expect(mockEmail.sendWelcome).toHaveBeenCalledWith(user.email)
|
|
326
|
+
expect(result).toEqual(user)
|
|
327
|
+
})
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Factory Pattern
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
interface Services {
|
|
334
|
+
userRepo: UserRepository
|
|
335
|
+
emailService: EmailService
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function createUserService(deps: Services) {
|
|
339
|
+
return {
|
|
340
|
+
async createUser(data: CreateUserInput) {
|
|
341
|
+
const user = await deps.userRepo.save(data)
|
|
342
|
+
await deps.emailService.sendWelcome(user.email)
|
|
343
|
+
return user
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Test with mocked dependencies
|
|
349
|
+
it('creates user with factory', async () => {
|
|
350
|
+
const service = createUserService({
|
|
351
|
+
userRepo: mockRepo,
|
|
352
|
+
emailService: mockEmail,
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
await service.createUser(userData)
|
|
356
|
+
expect(mockRepo.save).toHaveBeenCalled()
|
|
357
|
+
})
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## Mock Assertions
|
|
361
|
+
|
|
362
|
+
### Call Tracking
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
// Called at all
|
|
366
|
+
expect(mockFn).toHaveBeenCalled()
|
|
367
|
+
expect(mockFn).not.toHaveBeenCalled()
|
|
368
|
+
|
|
369
|
+
// Call count
|
|
370
|
+
expect(mockFn).toHaveBeenCalledTimes(3)
|
|
371
|
+
expect(mockFn).toHaveBeenCalledOnce()
|
|
372
|
+
|
|
373
|
+
// Call arguments
|
|
374
|
+
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2')
|
|
375
|
+
expect(mockFn).toHaveBeenLastCalledWith('arg1', 'arg2')
|
|
376
|
+
expect(mockFn).toHaveBeenNthCalledWith(2, 'arg1', 'arg2') // second call
|
|
377
|
+
|
|
378
|
+
// Partial argument matching
|
|
379
|
+
expect(mockFn).toHaveBeenCalledWith(
|
|
380
|
+
expect.objectContaining({ id: '123' }),
|
|
381
|
+
expect.any(Number)
|
|
382
|
+
)
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Call Order
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
const mockA = vi.fn()
|
|
389
|
+
const mockB = vi.fn()
|
|
390
|
+
|
|
391
|
+
mockA('first')
|
|
392
|
+
mockB('second')
|
|
393
|
+
mockA('third')
|
|
394
|
+
|
|
395
|
+
// Check call order
|
|
396
|
+
expect(mockA.mock.calls[0][0]).toBe('first')
|
|
397
|
+
expect(mockB.mock.calls[0][0]).toBe('second')
|
|
398
|
+
expect(mockA.mock.calls[1][0]).toBe('third')
|
|
399
|
+
|
|
400
|
+
// Or use invocationCallOrder
|
|
401
|
+
expect(mockA.mock.invocationCallOrder[0]).toBeLessThan(
|
|
402
|
+
mockB.mock.invocationCallOrder[0]
|
|
403
|
+
)
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Return Values
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
mockFn.mockReturnValue('result')
|
|
410
|
+
const result = mockFn()
|
|
411
|
+
|
|
412
|
+
expect(mockFn).toHaveReturned()
|
|
413
|
+
expect(mockFn).toHaveReturnedWith('result')
|
|
414
|
+
expect(mockFn).toHaveLastReturnedWith('result')
|
|
415
|
+
expect(mockFn).toHaveNthReturnedWith(1, 'result')
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
## Mock State Management
|
|
419
|
+
|
|
420
|
+
### Resetting Mocks
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
// Clear call history and results, keep implementation
|
|
424
|
+
mockFn.mockClear()
|
|
425
|
+
|
|
426
|
+
// Clear everything, reset to no implementation
|
|
427
|
+
mockFn.mockReset()
|
|
428
|
+
|
|
429
|
+
// Reset and restore to original implementation (for spies)
|
|
430
|
+
spyFn.mockRestore()
|
|
431
|
+
|
|
432
|
+
// In tests
|
|
433
|
+
beforeEach(() => {
|
|
434
|
+
vi.clearAllMocks() // Clear all mocks
|
|
435
|
+
// or
|
|
436
|
+
vi.resetAllMocks() // Reset all mocks
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
afterAll(() => {
|
|
440
|
+
vi.restoreAllMocks() // Restore all spies
|
|
441
|
+
})
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### Mock State Inspection
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
mockFn('arg1', 'arg2')
|
|
448
|
+
mockFn('arg3', 'arg4')
|
|
449
|
+
|
|
450
|
+
// Access mock state
|
|
451
|
+
console.log(mockFn.mock.calls) // [['arg1', 'arg2'], ['arg3', 'arg4']]
|
|
452
|
+
console.log(mockFn.mock.results) // [{ type: 'return', value: ... }]
|
|
453
|
+
console.log(mockFn.mock.lastCall) // ['arg3', 'arg4']
|
|
454
|
+
console.log(mockFn.mock.calls.length) // 2
|
|
455
|
+
|
|
456
|
+
// Check if it's a mock
|
|
457
|
+
expect(vi.isMockFunction(mockFn)).toBe(true)
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
## Global Mocks
|
|
461
|
+
|
|
462
|
+
### Mocking Global Objects
|
|
463
|
+
|
|
464
|
+
```typescript
|
|
465
|
+
// Mock global fetch
|
|
466
|
+
global.fetch = vi.fn()
|
|
467
|
+
|
|
468
|
+
// Type-safe global mock
|
|
469
|
+
global.fetch = vi.fn<typeof fetch>()
|
|
470
|
+
|
|
471
|
+
global.fetch.mockResolvedValue({
|
|
472
|
+
ok: true,
|
|
473
|
+
json: async () => ({ data: 'response' })
|
|
474
|
+
} as Response)
|
|
475
|
+
|
|
476
|
+
// In test
|
|
477
|
+
const data = await fetch('/api/users')
|
|
478
|
+
expect(global.fetch).toHaveBeenCalledWith('/api/users')
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### Mocking Environment Variables
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
import { beforeEach, afterEach } from 'vitest'
|
|
485
|
+
|
|
486
|
+
let originalEnv: NodeJS.ProcessEnv
|
|
487
|
+
|
|
488
|
+
beforeEach(() => {
|
|
489
|
+
originalEnv = process.env
|
|
490
|
+
process.env = { ...originalEnv, NODE_ENV: 'test' }
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
afterEach(() => {
|
|
494
|
+
process.env = originalEnv
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
it('uses test environment', () => {
|
|
498
|
+
expect(process.env.NODE_ENV).toBe('test')
|
|
499
|
+
})
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### Mocking Timers
|
|
503
|
+
|
|
504
|
+
```typescript
|
|
505
|
+
import { vi } from 'vitest'
|
|
506
|
+
|
|
507
|
+
beforeEach(() => {
|
|
508
|
+
vi.useFakeTimers()
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
afterEach(() => {
|
|
512
|
+
vi.useRealTimers()
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
it('executes after delay', async () => {
|
|
516
|
+
const callback = vi.fn()
|
|
517
|
+
|
|
518
|
+
setTimeout(callback, 1000)
|
|
519
|
+
|
|
520
|
+
// Time hasn't passed yet
|
|
521
|
+
expect(callback).not.toHaveBeenCalled()
|
|
522
|
+
|
|
523
|
+
// Advance time
|
|
524
|
+
vi.advanceTimersByTime(1000)
|
|
525
|
+
|
|
526
|
+
expect(callback).toHaveBeenCalled()
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
it('runs all timers', async () => {
|
|
530
|
+
const callback = vi.fn()
|
|
531
|
+
|
|
532
|
+
setTimeout(callback, 1000)
|
|
533
|
+
setTimeout(callback, 2000)
|
|
534
|
+
|
|
535
|
+
vi.runAllTimers()
|
|
536
|
+
|
|
537
|
+
expect(callback).toHaveBeenCalledTimes(2)
|
|
538
|
+
})
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
## Advanced Patterns
|
|
542
|
+
|
|
543
|
+
### Mocking Chains
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
const mockBuilder = {
|
|
547
|
+
select: vi.fn().mockReturnThis(),
|
|
548
|
+
where: vi.fn().mockReturnThis(),
|
|
549
|
+
limit: vi.fn().mockReturnThis(),
|
|
550
|
+
execute: vi.fn().mockResolvedValue([user]),
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Chainable API
|
|
554
|
+
const result = await mockBuilder
|
|
555
|
+
.select(['id', 'name'])
|
|
556
|
+
.where({ active: true })
|
|
557
|
+
.limit(10)
|
|
558
|
+
.execute()
|
|
559
|
+
|
|
560
|
+
expect(mockBuilder.select).toHaveBeenCalledWith(['id', 'name'])
|
|
561
|
+
expect(mockBuilder.where).toHaveBeenCalledWith({ active: true })
|
|
562
|
+
expect(mockBuilder.limit).toHaveBeenCalledWith(10)
|
|
563
|
+
expect(result).toEqual([user])
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
### Mock Generators
|
|
567
|
+
|
|
568
|
+
```typescript
|
|
569
|
+
function* numberGenerator() {
|
|
570
|
+
yield 1
|
|
571
|
+
yield 2
|
|
572
|
+
yield 3
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const mockGen = vi.fn(numberGenerator)
|
|
576
|
+
|
|
577
|
+
const gen = mockGen()
|
|
578
|
+
expect(gen.next().value).toBe(1)
|
|
579
|
+
expect(gen.next().value).toBe(2)
|
|
580
|
+
|
|
581
|
+
expect(mockGen).toHaveBeenCalledOnce()
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
### Async Iterators
|
|
585
|
+
|
|
586
|
+
```typescript
|
|
587
|
+
async function* asyncGenerator() {
|
|
588
|
+
yield 'a'
|
|
589
|
+
yield 'b'
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const mockAsyncGen = vi.fn(asyncGenerator)
|
|
593
|
+
|
|
594
|
+
const gen = mockAsyncGen()
|
|
595
|
+
expect((await gen.next()).value).toBe('a')
|
|
596
|
+
expect((await gen.next()).value).toBe('b')
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
## Guidelines
|
|
600
|
+
|
|
601
|
+
1. **Use `vi.mocked<T>()` for type safety** - Ensures mocks match interface contracts
|
|
602
|
+
2. **Mock at module boundaries** - Mock external dependencies, not internal functions
|
|
603
|
+
3. **Prefer dependency injection** - Makes testing easier and code more flexible
|
|
604
|
+
4. **Clear mocks between tests** - Use `beforeEach(() => vi.clearAllMocks())`
|
|
605
|
+
5. **Use spies for real implementations** - When you need to verify calls but keep behavior
|
|
606
|
+
6. **One mock setup per test** - Avoid shared mock state across tests
|
|
607
|
+
7. **Explicit return values** - Set up mock returns explicitly, don't rely on defaults
|
|
608
|
+
8. **Test both success and failure** - Mock both resolved and rejected scenarios
|
|
609
|
+
9. **Verify call order when critical** - Use `mock.invocationCallOrder` for sequence testing
|
|
610
|
+
10. **Restore spies after tests** - Use `afterAll(() => vi.restoreAllMocks())`
|
|
611
|
+
11. **Use fake timers for time-based code** - More reliable than real delays
|
|
612
|
+
12. **Mock at the right level** - HTTP layer (MSW) vs module layer vs function layer
|
|
613
|
+
|
|
614
|
+
## Common Pitfalls
|
|
615
|
+
|
|
616
|
+
### Don't Mock What You Don't Own
|
|
617
|
+
|
|
618
|
+
```typescript
|
|
619
|
+
// BAD - mocking third-party library internals
|
|
620
|
+
vi.mock('express', () => ({
|
|
621
|
+
Router: vi.fn() // fragile, breaks on updates
|
|
622
|
+
}))
|
|
623
|
+
|
|
624
|
+
// GOOD - create abstraction and mock that
|
|
625
|
+
interface HttpRouter {
|
|
626
|
+
get(path: string, handler: Function): void
|
|
627
|
+
post(path: string, handler: Function): void
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const mockRouter = vi.mocked<HttpRouter>({
|
|
631
|
+
get: vi.fn(),
|
|
632
|
+
post: vi.fn(),
|
|
633
|
+
})
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
### Avoid Over-Mocking
|
|
637
|
+
|
|
638
|
+
```typescript
|
|
639
|
+
// BAD - mocking everything
|
|
640
|
+
const mockUserService = {
|
|
641
|
+
create: vi.fn(),
|
|
642
|
+
validate: vi.fn(),
|
|
643
|
+
format: vi.fn(),
|
|
644
|
+
serialize: vi.fn(),
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// GOOD - mock external dependencies only
|
|
648
|
+
const mockUserRepo = vi.mocked<UserRepository>({
|
|
649
|
+
save: vi.fn()
|
|
650
|
+
})
|
|
651
|
+
|
|
652
|
+
const userService = new UserService(mockUserRepo) // test real logic
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
### Reset Mock State
|
|
656
|
+
|
|
657
|
+
```typescript
|
|
658
|
+
// BAD - shared mock state
|
|
659
|
+
const mockFn = vi.fn()
|
|
660
|
+
|
|
661
|
+
it('test 1', () => {
|
|
662
|
+
mockFn('arg1')
|
|
663
|
+
expect(mockFn).toHaveBeenCalledOnce()
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
it('test 2', () => {
|
|
667
|
+
// FAILS - mockFn still has call from test 1
|
|
668
|
+
expect(mockFn).not.toHaveBeenCalled()
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
// GOOD - clear between tests
|
|
672
|
+
beforeEach(() => {
|
|
673
|
+
vi.clearAllMocks()
|
|
674
|
+
})
|
|
675
|
+
```
|