Skip to content

Commit dfbb1b7

Browse files
committed
docs(testing): update testing best practices with performance rules
- document vi.hoisted + vi.mock + static import as the standard pattern - explicitly ban vi.resetModules, vi.doMock, vi.importActual, mockAuth, setupCommonApiMocks - document global mocks from vitest.setup.ts - add mock pattern reference for auth, hybrid auth, and database chains - add performance rules section covering heavy deps, jsdom vs node, real timers
1 parent 75b11d7 commit dfbb1b7

File tree

3 files changed

+406
-64
lines changed

3 files changed

+406
-64
lines changed

.claude/rules/sim-testing.md

Lines changed: 186 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,51 +8,210 @@ paths:
88

99
Use Vitest. Test files: `feature.ts``feature.test.ts`
1010

11+
## Global Mocks (vitest.setup.ts)
12+
13+
These modules are mocked globally — do NOT re-mock them in test files unless you need to override behavior:
14+
15+
- `@sim/db``databaseMock`
16+
- `drizzle-orm``drizzleOrmMock`
17+
- `@sim/logger``loggerMock`
18+
- `@/stores/console/store`, `@/stores/terminal`, `@/stores/execution/store`
19+
- `@/blocks/registry`
20+
- `@trigger.dev/sdk`
21+
1122
## Structure
1223

1324
```typescript
1425
/**
1526
* @vitest-environment node
1627
*/
17-
import { databaseMock, loggerMock } from '@sim/testing'
18-
import { describe, expect, it, vi } from 'vitest'
28+
import { createMockRequest } from '@sim/testing'
29+
import { beforeEach, describe, expect, it, vi } from 'vitest'
30+
31+
const { mockGetSession } = vi.hoisted(() => ({
32+
mockGetSession: vi.fn(),
33+
}))
1934

20-
vi.mock('@sim/db', () => databaseMock)
21-
vi.mock('@sim/logger', () => loggerMock)
35+
vi.mock('@/lib/auth', () => ({
36+
auth: { api: { getSession: vi.fn() } },
37+
getSession: mockGetSession,
38+
}))
2239

23-
import { myFunction } from '@/lib/feature'
40+
import { GET, POST } from '@/app/api/my-route/route'
2441

25-
describe('myFunction', () => {
26-
beforeEach(() => vi.clearAllMocks())
27-
it.concurrent('isolated tests run in parallel', () => { ... })
42+
describe('my route', () => {
43+
beforeEach(() => {
44+
vi.clearAllMocks()
45+
mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
46+
})
47+
48+
it('returns data', async () => {
49+
const req = createMockRequest('GET')
50+
const res = await GET(req)
51+
expect(res.status).toBe(200)
52+
})
2853
})
2954
```
3055

31-
## @sim/testing Package
56+
## Performance Rules (Critical)
3257

33-
Always prefer over local mocks.
58+
### NEVER use `vi.resetModules()` + `vi.doMock()` + `await import()`
3459

35-
| Category | Utilities |
36-
|----------|-----------|
37-
| **Mocks** | `loggerMock`, `databaseMock`, `setupGlobalFetchMock()` |
38-
| **Factories** | `createSession()`, `createWorkflowRecord()`, `createBlock()`, `createExecutorContext()` |
39-
| **Builders** | `WorkflowBuilder`, `ExecutionContextBuilder` |
40-
| **Assertions** | `expectWorkflowAccessGranted()`, `expectBlockExecuted()` |
60+
This is the #1 cause of slow tests. It forces complete module re-evaluation per test.
61+
62+
```typescript
63+
// BAD — forces module re-evaluation every test (~50-100ms each)
64+
beforeEach(() => {
65+
vi.resetModules()
66+
vi.doMock('@/lib/auth', () => ({ getSession: vi.fn() }))
67+
})
68+
it('test', async () => {
69+
const { GET } = await import('./route') // slow dynamic import
70+
})
71+
72+
// GOOD — module loaded once, mocks reconfigured per test (~1ms each)
73+
const { mockGetSession } = vi.hoisted(() => ({
74+
mockGetSession: vi.fn(),
75+
}))
76+
vi.mock('@/lib/auth', () => ({ getSession: mockGetSession }))
77+
import { GET } from '@/app/api/my-route/route'
78+
79+
beforeEach(() => { vi.clearAllMocks() })
80+
it('test', () => {
81+
mockGetSession.mockResolvedValue({ user: { id: '1' } })
82+
})
83+
```
84+
85+
**Only exception:** Singleton modules that cache state at module scope (e.g., Redis clients, connection pools). These genuinely need `vi.resetModules()` + dynamic import to get a fresh instance per test.
86+
87+
### NEVER use `vi.importActual()`
88+
89+
This defeats the purpose of mocking by loading the real module and all its dependencies.
90+
91+
```typescript
92+
// BAD — loads real module + all transitive deps
93+
vi.mock('@/lib/workspaces/utils', async () => {
94+
const actual = await vi.importActual('@/lib/workspaces/utils')
95+
return { ...actual, myFn: vi.fn() }
96+
})
97+
98+
// GOOD — mock everything, only implement what tests need
99+
vi.mock('@/lib/workspaces/utils', () => ({
100+
myFn: vi.fn(),
101+
otherFn: vi.fn(),
102+
}))
103+
```
104+
105+
### NEVER use `mockAuth()`, `mockConsoleLogger()`, or `setupCommonApiMocks()` from `@sim/testing`
106+
107+
These helpers internally use `vi.doMock()` which is slow. Use direct `vi.hoisted()` + `vi.mock()` instead.
108+
109+
### Mock heavy transitive dependencies
110+
111+
If a module under test imports `@/blocks` (200+ files), `@/tools/registry`, or other heavy modules, mock them:
112+
113+
```typescript
114+
vi.mock('@/blocks', () => ({
115+
getBlock: () => null,
116+
getAllBlocks: () => ({}),
117+
getAllBlockTypes: () => [],
118+
registry: {},
119+
}))
120+
```
121+
122+
### Use `@vitest-environment node` unless DOM is needed
123+
124+
Only use `@vitest-environment jsdom` if the test uses `window`, `document`, `FormData`, or other browser APIs. Node environment is significantly faster.
125+
126+
### Avoid real timers in tests
127+
128+
```typescript
129+
// BAD
130+
await new Promise(r => setTimeout(r, 500))
131+
132+
// GOOD — use minimal delays or fake timers
133+
await new Promise(r => setTimeout(r, 1))
134+
// or
135+
vi.useFakeTimers()
136+
```
137+
138+
## Mock Pattern Reference
139+
140+
### Auth mocking (API routes)
141+
142+
```typescript
143+
const { mockGetSession } = vi.hoisted(() => ({
144+
mockGetSession: vi.fn(),
145+
}))
146+
147+
vi.mock('@/lib/auth', () => ({
148+
auth: { api: { getSession: vi.fn() } },
149+
getSession: mockGetSession,
150+
}))
151+
152+
// In tests:
153+
mockGetSession.mockResolvedValue({ user: { id: 'user-1', email: 'test@example.com' } })
154+
mockGetSession.mockResolvedValue(null) // unauthenticated
155+
```
156+
157+
### Hybrid auth mocking
41158

42-
## Rules
159+
```typescript
160+
const { mockCheckSessionOrInternalAuth } = vi.hoisted(() => ({
161+
mockCheckSessionOrInternalAuth: vi.fn(),
162+
}))
43163

44-
1. `@vitest-environment node` directive at file top
45-
2. `vi.mock()` calls before importing mocked modules
46-
3. `@sim/testing` utilities over local mocks
47-
4. `it.concurrent` for isolated tests (no shared mutable state)
48-
5. `beforeEach(() => vi.clearAllMocks())` to reset state
164+
vi.mock('@/lib/auth/hybrid', () => ({
165+
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
166+
}))
49167

50-
## Hoisted Mocks
168+
// In tests:
169+
mockCheckSessionOrInternalAuth.mockResolvedValue({
170+
success: true, userId: 'user-1', authType: 'session',
171+
})
172+
```
51173

52-
For mutable mock references:
174+
### Database chain mocking
53175

54176
```typescript
55-
const mockFn = vi.hoisted(() => vi.fn())
56-
vi.mock('@/lib/module', () => ({ myFunction: mockFn }))
57-
mockFn.mockResolvedValue({ data: 'test' })
177+
const { mockSelect, mockFrom, mockWhere } = vi.hoisted(() => ({
178+
mockSelect: vi.fn(),
179+
mockFrom: vi.fn(),
180+
mockWhere: vi.fn(),
181+
}))
182+
183+
vi.mock('@sim/db', () => ({
184+
db: { select: mockSelect },
185+
}))
186+
187+
beforeEach(() => {
188+
mockSelect.mockReturnValue({ from: mockFrom })
189+
mockFrom.mockReturnValue({ where: mockWhere })
190+
mockWhere.mockResolvedValue([{ id: '1', name: 'test' }])
191+
})
58192
```
193+
194+
## @sim/testing Package
195+
196+
Always prefer over local test data.
197+
198+
| Category | Utilities |
199+
|----------|-----------|
200+
| **Mocks** | `loggerMock`, `databaseMock`, `drizzleOrmMock`, `setupGlobalFetchMock()` |
201+
| **Factories** | `createSession()`, `createWorkflowRecord()`, `createBlock()`, `createExecutionContext()` |
202+
| **Builders** | `WorkflowBuilder`, `ExecutionContextBuilder` |
203+
| **Assertions** | `expectWorkflowAccessGranted()`, `expectBlockExecuted()` |
204+
| **Requests** | `createMockRequest()`, `createEnvMock()` |
205+
206+
## Rules Summary
207+
208+
1. `@vitest-environment node` unless DOM is required
209+
2. `vi.hoisted()` + `vi.mock()` + static imports — never `vi.resetModules()` + `vi.doMock()` + dynamic imports
210+
3. `vi.mock()` calls before importing mocked modules
211+
4. `@sim/testing` utilities over local mocks
212+
5. `beforeEach(() => vi.clearAllMocks())` to reset state — no redundant `afterEach`
213+
6. No `vi.importActual()` — mock everything explicitly
214+
7. No `mockAuth()`, `mockConsoleLogger()`, `setupCommonApiMocks()` — use direct mocks
215+
8. Mock heavy deps (`@/blocks`, `@/tools/registry`, `@/triggers`) in tests that don't need them
216+
9. Use absolute imports in test files
217+
10. Avoid real timers — use 1ms delays or `vi.useFakeTimers()`

0 commit comments

Comments
 (0)