create-kuckit-app 2.0.2 → 2.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/README.md +24 -14
- package/package.json +1 -1
- package/templates/base/.claude/skills/beads/CLAUDE.md +87 -0
- package/templates/base/.claude/skills/beads/README.md +123 -0
- package/templates/base/.claude/skills/beads/SKILL.md +77 -715
- package/templates/base/.claude/skills/beads/adr/0001-bd-prime-as-source-of-truth.md +61 -0
- package/templates/base/.claude/skills/beads/resources/AGENTS.md +62 -0
- package/templates/base/.claude/skills/beads/resources/ASYNC_GATES.md +175 -0
- package/templates/base/.claude/skills/beads/resources/BOUNDARIES.md +520 -0
- package/templates/base/.claude/skills/beads/resources/CHEMISTRY_PATTERNS.md +197 -0
- package/templates/base/.claude/skills/beads/resources/CLI_REFERENCE.md +561 -0
- package/templates/base/.claude/skills/beads/resources/DEPENDENCIES.md +754 -0
- package/templates/base/.claude/skills/beads/resources/INTEGRATION_PATTERNS.md +438 -0
- package/templates/base/.claude/skills/beads/resources/ISSUE_CREATION.md +150 -0
- package/templates/base/.claude/skills/beads/resources/MOLECULES.md +370 -0
- package/templates/base/.claude/skills/beads/resources/PATTERNS.md +363 -0
- package/templates/base/.claude/skills/beads/resources/RESUMABILITY.md +239 -0
- package/templates/base/.claude/skills/beads/resources/STATIC_DATA.md +61 -0
- package/templates/base/.claude/skills/beads/resources/TROUBLESHOOTING.md +537 -0
- package/templates/base/.claude/skills/beads/resources/WORKFLOWS.md +638 -0
- package/templates/base/.claude/skills/beads/resources/WORKTREES.md +95 -0
- package/templates/base/.claude/skills/browser-skill/SKILL.md +72 -0
- package/templates/base/.claude/skills/knowledge/SKILL.md +155 -205
- package/templates/base/.claude/skills/knowledge/reference/doc-mapping.md +49 -0
- package/templates/base/.claude/skills/knowledge/reference/extraction-prompts.md +102 -0
- package/templates/base/.claude/skills/kuckit/SKILL.md +15 -9
- package/templates/base/.claude/skills/kuckit/references/MODULE-DEVELOPMENT.md +142 -0
- package/templates/base/.claude/skills/kuckit/references/PACKAGES.md +22 -17
- package/templates/base/.claude/skills/kuckit/references/PUBLISHING.md +92 -0
- package/templates/base/.claude/skills/module-testing/SKILL.md +1 -1
- package/templates/base/.claude/skills/planning/SKILL.md +26 -1
- package/templates/base/.env.example +1 -1
- package/templates/base/AGENTS.md +155 -418
- package/templates/base/apps/server/src/modules.ts +14 -1
- package/templates/base/apps/web/.env.example +1 -1
- package/templates/base/apps/web/src/routes/$.tsx +0 -1
- package/templates/base/apps/web/src/routes/dashboard.tsx +3 -1
- package/templates/base/docs/ARCHITECTURE.md +689 -0
- package/templates/base/docs/DEPENDENCY-INJECTION.md +871 -0
- package/templates/base/docs/DEPLOYMENT.md +573 -0
- package/templates/base/docs/INDEX.md +135 -0
- package/templates/base/docs/MIGRATION.md +989 -0
- package/templates/base/docs/MODULE_CSS.md +343 -0
- package/templates/base/docs/MODULE_TESTING.md +368 -0
- package/templates/base/docs/MULTI_AGENT_WORKFLOW.md +909 -0
- package/templates/base/docs/TESTING.md +579 -0
- package/templates/base/docs/TROUBLESHOOTING.md +360 -0
- package/templates/base/package.json +2 -0
- package/templates/base/packages/items-module/AGENTS.md +3 -1
- package/templates/base/packages/items-module/src/server/adapters/{item.drizzle.ts → item.repository.ts} +1 -13
- package/templates/base/packages/items-module/src/server/module.ts +2 -1
- package/templates/base/packages/items-module/src/server/schema/item.ts +13 -0
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
# Testing Guide
|
|
2
|
+
|
|
3
|
+
This guide covers testing strategies for Kuckit applications, from unit tests to integration testing.
|
|
4
|
+
|
|
5
|
+
> **Module Testing**: For testing standalone npm modules before publishing, see [Module Testing Guide](./MODULE_TESTING.md).
|
|
6
|
+
|
|
7
|
+
## Quick Reference
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Run all tests
|
|
11
|
+
bun test
|
|
12
|
+
|
|
13
|
+
# Run tests in a specific package
|
|
14
|
+
bun test --cwd packages/my-module
|
|
15
|
+
|
|
16
|
+
# Run with coverage
|
|
17
|
+
bun test --coverage
|
|
18
|
+
|
|
19
|
+
# Watch mode
|
|
20
|
+
bun test --watch
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Testing Philosophy
|
|
26
|
+
|
|
27
|
+
Kuckit follows Clean Architecture, which naturally creates testable boundaries:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
31
|
+
│ API Layer │ ← Integration tests
|
|
32
|
+
│ (oRPC routers, Express handlers) │
|
|
33
|
+
├─────────────────────────────────────────────────────────────┤
|
|
34
|
+
│ Application Layer │ ← Unit tests (use cases)
|
|
35
|
+
│ (Use cases, application services) │
|
|
36
|
+
├─────────────────────────────────────────────────────────────┤
|
|
37
|
+
│ Domain Layer │ ← Unit tests (pure logic)
|
|
38
|
+
│ (Entities, value objects, domain services) │
|
|
39
|
+
├─────────────────────────────────────────────────────────────┤
|
|
40
|
+
│ Infrastructure Layer │ ← Integration tests
|
|
41
|
+
│ (Repositories, external services) │
|
|
42
|
+
└─────────────────────────────────────────────────────────────┘
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Test Pyramid
|
|
46
|
+
|
|
47
|
+
| Level | Focus | Speed | Coverage |
|
|
48
|
+
| ----------- | ------------------------- | ------ | -------- |
|
|
49
|
+
| Unit | Domain logic, use cases | Fast | High |
|
|
50
|
+
| Integration | Repository, API endpoints | Medium | Medium |
|
|
51
|
+
| E2E | Full user flows | Slow | Low |
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Unit Testing
|
|
56
|
+
|
|
57
|
+
### Domain Layer Tests
|
|
58
|
+
|
|
59
|
+
Domain entities contain pure business logic and are the easiest to test:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// packages/my-module/src/server/domain/entities/invoice.test.ts
|
|
63
|
+
import { describe, expect, it } from 'bun:test'
|
|
64
|
+
import { Invoice } from './invoice'
|
|
65
|
+
|
|
66
|
+
describe('Invoice', () => {
|
|
67
|
+
it('calculates total from line items', () => {
|
|
68
|
+
const invoice = Invoice.create({
|
|
69
|
+
lineItems: [
|
|
70
|
+
{ description: 'Item 1', amount: 100 },
|
|
71
|
+
{ description: 'Item 2', amount: 50 },
|
|
72
|
+
],
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
expect(invoice.total).toBe(150)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('applies discount correctly', () => {
|
|
79
|
+
const invoice = Invoice.create({
|
|
80
|
+
lineItems: [{ description: 'Item', amount: 100 }],
|
|
81
|
+
discountPercent: 10,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
expect(invoice.total).toBe(90)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('throws on negative amounts', () => {
|
|
88
|
+
expect(() =>
|
|
89
|
+
Invoice.create({
|
|
90
|
+
lineItems: [{ description: 'Item', amount: -50 }],
|
|
91
|
+
})
|
|
92
|
+
).toThrow('Amount must be positive')
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Use Case Tests
|
|
98
|
+
|
|
99
|
+
Use cases orchestrate domain logic and interact with ports. Mock the ports:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
// packages/my-module/src/server/usecases/create-invoice.test.ts
|
|
103
|
+
import { describe, expect, it, mock } from 'bun:test'
|
|
104
|
+
import { CreateInvoiceUseCase } from './create-invoice'
|
|
105
|
+
import type { InvoiceRepository } from '../ports/invoice-repository'
|
|
106
|
+
import type { Logger } from '@kuckit/domain'
|
|
107
|
+
|
|
108
|
+
describe('CreateInvoiceUseCase', () => {
|
|
109
|
+
it('creates invoice and persists it', async () => {
|
|
110
|
+
const mockRepo: InvoiceRepository = {
|
|
111
|
+
save: mock(() => Promise.resolve()),
|
|
112
|
+
findById: mock(() => Promise.resolve(null)),
|
|
113
|
+
findAll: mock(() => Promise.resolve([])),
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const mockLogger: Logger = {
|
|
117
|
+
info: mock(() => {}),
|
|
118
|
+
warn: mock(() => {}),
|
|
119
|
+
error: mock(() => {}),
|
|
120
|
+
debug: mock(() => {}),
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const useCase = new CreateInvoiceUseCase(mockRepo, mockLogger)
|
|
124
|
+
|
|
125
|
+
const result = await useCase.execute({
|
|
126
|
+
customerId: 'cust-123',
|
|
127
|
+
lineItems: [{ description: 'Service', amount: 500 }],
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
expect(result.total).toBe(500)
|
|
131
|
+
expect(mockRepo.save).toHaveBeenCalledTimes(1)
|
|
132
|
+
expect(mockLogger.info).toHaveBeenCalled()
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('throws when customer not found', async () => {
|
|
136
|
+
const mockRepo: InvoiceRepository = {
|
|
137
|
+
save: mock(() => Promise.reject(new Error('Customer not found'))),
|
|
138
|
+
findById: mock(() => Promise.resolve(null)),
|
|
139
|
+
findAll: mock(() => Promise.resolve([])),
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const useCase = new CreateInvoiceUseCase(mockRepo, {
|
|
143
|
+
info: () => {},
|
|
144
|
+
warn: () => {},
|
|
145
|
+
error: () => {},
|
|
146
|
+
debug: () => {},
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
await expect(useCase.execute({ customerId: 'invalid', lineItems: [] })).rejects.toThrow(
|
|
150
|
+
'Customer not found'
|
|
151
|
+
)
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Testing with DI Container
|
|
157
|
+
|
|
158
|
+
When you need the full container for tests:
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
// packages/my-module/src/server/__tests__/integration.test.ts
|
|
162
|
+
import { describe, expect, it, beforeAll, afterAll } from 'bun:test'
|
|
163
|
+
import { createContainer, asFunction, asValue } from 'awilix'
|
|
164
|
+
import { createTestDatabase } from '../test-utils/database'
|
|
165
|
+
|
|
166
|
+
describe('Module Integration', () => {
|
|
167
|
+
let container: ReturnType<typeof createContainer>
|
|
168
|
+
let testDb: Awaited<ReturnType<typeof createTestDatabase>>
|
|
169
|
+
|
|
170
|
+
beforeAll(async () => {
|
|
171
|
+
testDb = await createTestDatabase()
|
|
172
|
+
|
|
173
|
+
container = createContainer()
|
|
174
|
+
container.register({
|
|
175
|
+
db: asValue(testDb.db),
|
|
176
|
+
logger: asValue({ info: () => {}, warn: () => {}, error: () => {}, debug: () => {} }),
|
|
177
|
+
invoiceRepository: asFunction(({ db }) => new DrizzleInvoiceRepository(db)).scoped(),
|
|
178
|
+
createInvoiceUseCase: asFunction(
|
|
179
|
+
({ invoiceRepository, logger }) => new CreateInvoiceUseCase(invoiceRepository, logger)
|
|
180
|
+
).scoped(),
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
afterAll(async () => {
|
|
185
|
+
await testDb.cleanup()
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('creates invoice through use case', async () => {
|
|
189
|
+
const useCase = container.resolve('createInvoiceUseCase')
|
|
190
|
+
const result = await useCase.execute({
|
|
191
|
+
customerId: 'test-customer',
|
|
192
|
+
lineItems: [{ description: 'Test', amount: 100 }],
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
expect(result.id).toBeDefined()
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Integration Testing
|
|
203
|
+
|
|
204
|
+
### Repository Tests
|
|
205
|
+
|
|
206
|
+
Test repositories against a real database:
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
// packages/my-module/src/server/adapters/__tests__/invoice-repository.test.ts
|
|
210
|
+
import { describe, expect, it, beforeAll, afterAll, beforeEach } from 'bun:test'
|
|
211
|
+
import { DrizzleInvoiceRepository } from '../invoice-repository'
|
|
212
|
+
import { createTestDatabase, cleanupTestData } from '../../test-utils/database'
|
|
213
|
+
|
|
214
|
+
describe('DrizzleInvoiceRepository', () => {
|
|
215
|
+
let repository: DrizzleInvoiceRepository
|
|
216
|
+
let testDb: Awaited<ReturnType<typeof createTestDatabase>>
|
|
217
|
+
|
|
218
|
+
beforeAll(async () => {
|
|
219
|
+
testDb = await createTestDatabase()
|
|
220
|
+
repository = new DrizzleInvoiceRepository(testDb.db)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
afterAll(async () => {
|
|
224
|
+
await testDb.cleanup()
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
beforeEach(async () => {
|
|
228
|
+
await cleanupTestData(testDb.db)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('saves and retrieves invoice', async () => {
|
|
232
|
+
const invoice = Invoice.create({
|
|
233
|
+
customerId: 'cust-123',
|
|
234
|
+
lineItems: [{ description: 'Test', amount: 100 }],
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
await repository.save(invoice)
|
|
238
|
+
|
|
239
|
+
const retrieved = await repository.findById(invoice.id)
|
|
240
|
+
expect(retrieved).toBeDefined()
|
|
241
|
+
expect(retrieved?.total).toBe(100)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('returns null for non-existent invoice', async () => {
|
|
245
|
+
const result = await repository.findById('non-existent-id')
|
|
246
|
+
expect(result).toBeNull()
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Test Database Utilities
|
|
252
|
+
|
|
253
|
+
Create a reusable test database helper:
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
// packages/my-module/src/server/test-utils/database.ts
|
|
257
|
+
import { drizzle } from 'drizzle-orm/node-postgres'
|
|
258
|
+
import { Pool } from 'pg'
|
|
259
|
+
import * as schema from '../adapters/schema'
|
|
260
|
+
|
|
261
|
+
export async function createTestDatabase() {
|
|
262
|
+
const testDbUrl =
|
|
263
|
+
process.env.TEST_DATABASE_URL ?? 'postgresql://postgres:postgres@localhost:5432/kuckit_test'
|
|
264
|
+
|
|
265
|
+
const pool = new Pool({ connectionString: testDbUrl })
|
|
266
|
+
const db = drizzle(pool, { schema })
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
db,
|
|
270
|
+
pool,
|
|
271
|
+
cleanup: async () => {
|
|
272
|
+
await pool.end()
|
|
273
|
+
},
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export async function cleanupTestData(db: ReturnType<typeof drizzle>) {
|
|
278
|
+
await db.delete(schema.invoices)
|
|
279
|
+
await db.delete(schema.lineItems)
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### API Endpoint Tests
|
|
284
|
+
|
|
285
|
+
Test oRPC routers with a test client:
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
// packages/my-module/src/server/api/__tests__/invoices-router.test.ts
|
|
289
|
+
import { describe, expect, it, beforeAll, afterAll } from 'bun:test'
|
|
290
|
+
import { createORPCFetchClient } from '@orpc/client'
|
|
291
|
+
import type { InvoicesRouter } from '../invoices-router'
|
|
292
|
+
|
|
293
|
+
describe('Invoices API', () => {
|
|
294
|
+
let client: ReturnType<typeof createORPCFetchClient<InvoicesRouter>>
|
|
295
|
+
let server: any
|
|
296
|
+
|
|
297
|
+
beforeAll(async () => {
|
|
298
|
+
// Start test server
|
|
299
|
+
server = await startTestServer()
|
|
300
|
+
client = createORPCFetchClient<InvoicesRouter>({
|
|
301
|
+
baseURL: `http://localhost:${server.port}/rpc/invoices`,
|
|
302
|
+
})
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
afterAll(async () => {
|
|
306
|
+
await server.close()
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('creates invoice via API', async () => {
|
|
310
|
+
const result = await client.create({
|
|
311
|
+
customerId: 'cust-123',
|
|
312
|
+
lineItems: [{ description: 'API Test', amount: 250 }],
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
expect(result.id).toBeDefined()
|
|
316
|
+
expect(result.total).toBe(250)
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('returns 404 for non-existent invoice', async () => {
|
|
320
|
+
await expect(client.getById({ id: 'non-existent' })).rejects.toThrow(/not found/i)
|
|
321
|
+
})
|
|
322
|
+
})
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## Test Configuration
|
|
328
|
+
|
|
329
|
+
### Bun Test Setup
|
|
330
|
+
|
|
331
|
+
Create `bunfig.toml` in your project root:
|
|
332
|
+
|
|
333
|
+
```toml
|
|
334
|
+
[test]
|
|
335
|
+
preload = ["./test-setup.ts"]
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Create `test-setup.ts` for global setup:
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
// test-setup.ts
|
|
342
|
+
import { beforeAll, afterAll } from 'bun:test'
|
|
343
|
+
|
|
344
|
+
beforeAll(async () => {
|
|
345
|
+
// Global setup: start containers, seed data, etc.
|
|
346
|
+
console.log('🧪 Starting test suite...')
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
afterAll(async () => {
|
|
350
|
+
// Global cleanup
|
|
351
|
+
console.log('✅ Test suite complete')
|
|
352
|
+
})
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Environment Variables for Tests
|
|
356
|
+
|
|
357
|
+
Create `.env.test`:
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
# Test database (separate from dev)
|
|
361
|
+
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/kuckit_test
|
|
362
|
+
TEST_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/kuckit_test
|
|
363
|
+
|
|
364
|
+
# Auth (test values)
|
|
365
|
+
BETTER_AUTH_SECRET=test-secret-for-testing-only
|
|
366
|
+
BETTER_AUTH_URL=http://localhost:3000
|
|
367
|
+
|
|
368
|
+
# Disable external services in tests
|
|
369
|
+
DISABLE_EXTERNAL_APIS=true
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Load in tests:
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
import { beforeAll } from 'bun:test'
|
|
376
|
+
|
|
377
|
+
beforeAll(() => {
|
|
378
|
+
// Load test environment
|
|
379
|
+
process.loadEnvFile('.env.test')
|
|
380
|
+
})
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## Test Patterns
|
|
386
|
+
|
|
387
|
+
### Factories for Test Data
|
|
388
|
+
|
|
389
|
+
Create factories to generate test data:
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
// packages/my-module/src/server/test-utils/factories.ts
|
|
393
|
+
import { Invoice } from '../domain/entities/invoice'
|
|
394
|
+
|
|
395
|
+
let invoiceCounter = 0
|
|
396
|
+
|
|
397
|
+
export function createTestInvoice(overrides: Partial<InvoiceProps> = {}): Invoice {
|
|
398
|
+
invoiceCounter++
|
|
399
|
+
return Invoice.create({
|
|
400
|
+
id: `test-invoice-${invoiceCounter}`,
|
|
401
|
+
customerId: `test-customer-${invoiceCounter}`,
|
|
402
|
+
lineItems: [{ description: 'Test Item', amount: 100 }],
|
|
403
|
+
...overrides,
|
|
404
|
+
})
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function createTestLineItem(overrides: Partial<LineItemProps> = {}) {
|
|
408
|
+
return {
|
|
409
|
+
description: 'Test Line Item',
|
|
410
|
+
amount: 100,
|
|
411
|
+
quantity: 1,
|
|
412
|
+
...overrides,
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### Snapshot Testing
|
|
418
|
+
|
|
419
|
+
Use snapshots for complex outputs:
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
import { describe, expect, it } from 'bun:test'
|
|
423
|
+
|
|
424
|
+
describe('Invoice formatting', () => {
|
|
425
|
+
it('formats invoice as JSON', () => {
|
|
426
|
+
const invoice = createTestInvoice({
|
|
427
|
+
lineItems: [
|
|
428
|
+
{ description: 'Service A', amount: 100 },
|
|
429
|
+
{ description: 'Service B', amount: 200 },
|
|
430
|
+
],
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
expect(invoice.toJSON()).toMatchSnapshot()
|
|
434
|
+
})
|
|
435
|
+
})
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Testing Async Operations
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
import { describe, expect, it } from 'bun:test'
|
|
442
|
+
|
|
443
|
+
describe('Async operations', () => {
|
|
444
|
+
it('handles timeout correctly', async () => {
|
|
445
|
+
const slowOperation = new Promise((resolve) => setTimeout(() => resolve('done'), 100))
|
|
446
|
+
|
|
447
|
+
const result = await slowOperation
|
|
448
|
+
expect(result).toBe('done')
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
it('rejects on error', async () => {
|
|
452
|
+
const failingOperation = Promise.reject(new Error('Failed'))
|
|
453
|
+
|
|
454
|
+
await expect(failingOperation).rejects.toThrow('Failed')
|
|
455
|
+
})
|
|
456
|
+
})
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
## CI/CD Integration
|
|
462
|
+
|
|
463
|
+
### GitHub Actions Workflow
|
|
464
|
+
|
|
465
|
+
```yaml
|
|
466
|
+
# .github/workflows/test.yml
|
|
467
|
+
name: Tests
|
|
468
|
+
|
|
469
|
+
on:
|
|
470
|
+
push:
|
|
471
|
+
branches: [main]
|
|
472
|
+
pull_request:
|
|
473
|
+
branches: [main]
|
|
474
|
+
|
|
475
|
+
jobs:
|
|
476
|
+
test:
|
|
477
|
+
runs-on: ubuntu-latest
|
|
478
|
+
|
|
479
|
+
services:
|
|
480
|
+
postgres:
|
|
481
|
+
image: postgres:15
|
|
482
|
+
env:
|
|
483
|
+
POSTGRES_PASSWORD: postgres
|
|
484
|
+
POSTGRES_DB: kuckit_test
|
|
485
|
+
ports:
|
|
486
|
+
- 5432:5432
|
|
487
|
+
options: >-
|
|
488
|
+
--health-cmd pg_isready
|
|
489
|
+
--health-interval 10s
|
|
490
|
+
--health-timeout 5s
|
|
491
|
+
--health-retries 5
|
|
492
|
+
|
|
493
|
+
steps:
|
|
494
|
+
- uses: actions/checkout@v4
|
|
495
|
+
|
|
496
|
+
- uses: oven-sh/setup-bun@v2
|
|
497
|
+
with:
|
|
498
|
+
bun-version: latest
|
|
499
|
+
|
|
500
|
+
- name: Install dependencies
|
|
501
|
+
run: bun install
|
|
502
|
+
|
|
503
|
+
- name: Run type checks
|
|
504
|
+
run: bun run check-types
|
|
505
|
+
|
|
506
|
+
- name: Run tests
|
|
507
|
+
run: bun test --coverage
|
|
508
|
+
env:
|
|
509
|
+
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/kuckit_test
|
|
510
|
+
BETTER_AUTH_SECRET: test-secret
|
|
511
|
+
BETTER_AUTH_URL: http://localhost:3000
|
|
512
|
+
|
|
513
|
+
- name: Upload coverage
|
|
514
|
+
uses: codecov/codecov-action@v4
|
|
515
|
+
with:
|
|
516
|
+
files: ./coverage/coverage.json
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### Pre-commit Hooks
|
|
520
|
+
|
|
521
|
+
Add to `.husky/pre-commit`:
|
|
522
|
+
|
|
523
|
+
```bash
|
|
524
|
+
#!/usr/bin/env sh
|
|
525
|
+
. "$(dirname -- "$0")/_/husky.sh"
|
|
526
|
+
|
|
527
|
+
bun run check-types
|
|
528
|
+
bun test --bail
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
## Debugging Tests
|
|
534
|
+
|
|
535
|
+
### Verbose Output
|
|
536
|
+
|
|
537
|
+
```bash
|
|
538
|
+
# Run with verbose logging
|
|
539
|
+
bun test --verbose
|
|
540
|
+
|
|
541
|
+
# Run single test file
|
|
542
|
+
bun test packages/my-module/src/server/usecases/create-invoice.test.ts
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
### Debug Mode
|
|
546
|
+
|
|
547
|
+
```typescript
|
|
548
|
+
import { describe, it } from 'bun:test'
|
|
549
|
+
|
|
550
|
+
describe('Debugging', () => {
|
|
551
|
+
it('logs intermediate values', async () => {
|
|
552
|
+
const result = await someOperation()
|
|
553
|
+
|
|
554
|
+
// Debug output
|
|
555
|
+
console.log('Result:', JSON.stringify(result, null, 2))
|
|
556
|
+
|
|
557
|
+
expect(result).toBeDefined()
|
|
558
|
+
})
|
|
559
|
+
})
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
### Inspecting Test Database
|
|
563
|
+
|
|
564
|
+
```bash
|
|
565
|
+
# Connect to test database
|
|
566
|
+
psql postgresql://postgres:postgres@localhost:5432/kuckit_test
|
|
567
|
+
|
|
568
|
+
# Check table contents
|
|
569
|
+
SELECT * FROM invoices LIMIT 10;
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
---
|
|
573
|
+
|
|
574
|
+
## Related Documentation
|
|
575
|
+
|
|
576
|
+
- [Module Testing Guide](./MODULE_TESTING.md) - Testing modules before npm publish
|
|
577
|
+
- [Module Development](../packages/sdk/docs/MODULE_DEVELOPMENT.md) - Building modules
|
|
578
|
+
- [Architecture Overview](./ARCHITECTURE.md) - Clean Architecture layers
|
|
579
|
+
- [Troubleshooting](./TROUBLESHOOTING.md) - Common issues and fixes
|