@navios/core 0.3.0 → 0.5.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.
Files changed (96) hide show
  1. package/README.md +96 -3
  2. package/docs/README.md +310 -3
  3. package/docs/adapters.md +308 -0
  4. package/docs/application-setup.md +524 -0
  5. package/docs/attributes.md +689 -0
  6. package/docs/controllers.md +373 -0
  7. package/docs/endpoints.md +444 -0
  8. package/docs/exceptions.md +316 -0
  9. package/docs/guards.md +550 -0
  10. package/docs/modules.md +251 -0
  11. package/docs/quick-start.md +295 -0
  12. package/docs/services.md +428 -0
  13. package/docs/testing.md +704 -0
  14. package/lib/_tsup-dts-rollup.d.mts +313 -280
  15. package/lib/_tsup-dts-rollup.d.ts +313 -280
  16. package/lib/index.d.mts +47 -26
  17. package/lib/index.d.ts +47 -26
  18. package/lib/index.js +633 -1068
  19. package/lib/index.js.map +1 -1
  20. package/lib/index.mjs +632 -1061
  21. package/lib/index.mjs.map +1 -1
  22. package/package.json +11 -12
  23. package/project.json +17 -4
  24. package/src/__tests__/config.service.spec.mts +11 -9
  25. package/src/__tests__/controller.spec.mts +1 -2
  26. package/src/attribute.factory.mts +1 -1
  27. package/src/config/config.provider.mts +2 -2
  28. package/src/config/config.service.mts +4 -4
  29. package/src/decorators/controller.decorator.mts +1 -1
  30. package/src/decorators/endpoint.decorator.mts +9 -10
  31. package/src/decorators/header.decorator.mts +1 -1
  32. package/src/decorators/multipart.decorator.mts +5 -5
  33. package/src/decorators/stream.decorator.mts +5 -6
  34. package/src/factories/endpoint-adapter.factory.mts +21 -0
  35. package/src/factories/http-adapter.factory.mts +20 -0
  36. package/src/factories/index.mts +6 -0
  37. package/src/factories/multipart-adapter.factory.mts +21 -0
  38. package/src/factories/reply.factory.mts +21 -0
  39. package/src/factories/request.factory.mts +21 -0
  40. package/src/factories/stream-adapter.factory.mts +20 -0
  41. package/src/index.mts +1 -1
  42. package/src/interfaces/abstract-execution-context.inteface.mts +13 -0
  43. package/src/interfaces/abstract-http-adapter.interface.mts +20 -0
  44. package/src/interfaces/abstract-http-cors-options.interface.mts +59 -0
  45. package/src/interfaces/abstract-http-handler-adapter.interface.mts +13 -0
  46. package/src/interfaces/abstract-http-listen-options.interface.mts +4 -0
  47. package/src/interfaces/can-activate.mts +4 -2
  48. package/src/interfaces/http-header.mts +18 -0
  49. package/src/interfaces/index.mts +6 -0
  50. package/src/logger/console-logger.service.mts +28 -44
  51. package/src/logger/index.mts +1 -2
  52. package/src/logger/logger.service.mts +9 -128
  53. package/src/logger/logger.tokens.mts +21 -0
  54. package/src/metadata/handler.metadata.mts +7 -5
  55. package/src/navios.application.mts +65 -172
  56. package/src/navios.environment.mts +30 -0
  57. package/src/navios.factory.mts +53 -12
  58. package/src/services/guard-runner.service.mts +19 -9
  59. package/src/services/index.mts +0 -2
  60. package/src/services/module-loader.service.mts +4 -3
  61. package/src/tokens/endpoint-adapter.token.mts +8 -0
  62. package/src/tokens/execution-context.token.mts +2 -2
  63. package/src/tokens/http-adapter.token.mts +8 -0
  64. package/src/tokens/index.mts +4 -1
  65. package/src/tokens/multipart-adapter.token.mts +8 -0
  66. package/src/tokens/reply.token.mts +1 -5
  67. package/src/tokens/request.token.mts +1 -7
  68. package/src/tokens/stream-adapter.token.mts +8 -0
  69. package/tsconfig.json +6 -1
  70. package/tsconfig.lib.json +8 -0
  71. package/tsconfig.spec.json +12 -0
  72. package/tsup.config.mts +1 -0
  73. package/docs/recipes/prisma.md +0 -60
  74. package/examples/simple-test/api/index.mts +0 -64
  75. package/examples/simple-test/config/config.service.mts +0 -14
  76. package/examples/simple-test/config/configuration.mts +0 -7
  77. package/examples/simple-test/index.mts +0 -16
  78. package/examples/simple-test/src/acl/acl-modern.guard.mts +0 -15
  79. package/examples/simple-test/src/acl/acl.guard.mts +0 -14
  80. package/examples/simple-test/src/acl/app.guard.mts +0 -27
  81. package/examples/simple-test/src/acl/one-more.guard.mts +0 -15
  82. package/examples/simple-test/src/acl/public.attribute.mts +0 -21
  83. package/examples/simple-test/src/app.module.mts +0 -9
  84. package/examples/simple-test/src/user/user.controller.mts +0 -72
  85. package/examples/simple-test/src/user/user.module.mts +0 -14
  86. package/examples/simple-test/src/user/user.service.mts +0 -14
  87. package/src/adapters/endpoint-adapter.service.mts +0 -72
  88. package/src/adapters/handler-adapter.interface.mts +0 -21
  89. package/src/adapters/index.mts +0 -4
  90. package/src/adapters/multipart-adapter.service.mts +0 -131
  91. package/src/adapters/stream-adapter.service.mts +0 -91
  92. package/src/logger/logger.factory.mts +0 -36
  93. package/src/logger/pino-wrapper.mts +0 -64
  94. package/src/services/controller-adapter.service.mts +0 -124
  95. package/src/services/execution-context.mts +0 -54
  96. package/src/tokens/application.token.mts +0 -9
@@ -0,0 +1,704 @@
1
+ # Testing Guide
2
+
3
+ > This guide is for future reference and may be updated over time
4
+ >
5
+ > Not represent the final state of the testing strategy for Navios
6
+
7
+ This guide covers testing strategies and best practices for Navios applications, including unit testing, integration testing, and end-to-end testing.
8
+
9
+ ## Testing Philosophy
10
+
11
+ Navios promotes a testing-first approach with:
12
+
13
+ - **Unit Tests** - Test individual components in isolation
14
+ - **Integration Tests** - Test component interactions
15
+ - **End-to-End Tests** - Test complete user workflows
16
+
17
+ ## Setting Up Testing
18
+
19
+ ### Test Dependencies
20
+
21
+ ```bash
22
+ npm install --save-dev vitest supertest @navios/builder
23
+ ```
24
+
25
+ ## Unit Testing
26
+
27
+ ### Testing Services
28
+
29
+ ```typescript
30
+ import { TestContainer } from '@navios/core/testing'
31
+
32
+ import { DatabaseService } from './database.service.js'
33
+ import { UserService } from './user.service.js'
34
+
35
+ describe('UserService', () => {
36
+ let userService: UserService
37
+ let mockDatabase: vi.Mocked<DatabaseService>
38
+
39
+ beforeEach(() => {
40
+ const container = new TestContainer()
41
+
42
+ // Create mock database
43
+ mockDatabase = {
44
+ users: {
45
+ findUnique: vi.fn(),
46
+ findMany: vi.fn(),
47
+ create: vi.fn(),
48
+ update: vi.fn(),
49
+ delete: vi.fn(),
50
+ },
51
+ } as any
52
+
53
+ container.bind(DatabaseService).toValue(mockDatabase)
54
+ userService = container.get(UserService)
55
+ })
56
+
57
+ describe('findById', () => {
58
+ it('should return user when found', async () => {
59
+ const mockUser = { id: '1', name: 'John', email: 'john@test.com' }
60
+ mockDatabase.users.findUnique.mockResolvedValue(mockUser)
61
+
62
+ const result = await userService.findById('1')
63
+
64
+ expect(result).toEqual(mockUser)
65
+ expect(mockDatabase.users.findUnique).toHaveBeenCalledWith({
66
+ where: { id: '1' },
67
+ })
68
+ })
69
+
70
+ it('should return null when user not found', async () => {
71
+ mockDatabase.users.findUnique.mockResolvedValue(null)
72
+
73
+ const result = await userService.findById('1')
74
+
75
+ expect(result).toBeNull()
76
+ })
77
+ })
78
+
79
+ describe('create', () => {
80
+ it('should create and return new user', async () => {
81
+ const userData = { name: 'John', email: 'john@test.com' }
82
+ const createdUser = { id: '1', ...userData }
83
+
84
+ mockDatabase.users.create.mockResolvedValue(createdUser)
85
+
86
+ const result = await userService.create(userData)
87
+
88
+ expect(result).toEqual(createdUser)
89
+ expect(mockDatabase.users.create).toHaveBeenCalledWith({
90
+ data: userData,
91
+ })
92
+ })
93
+ })
94
+ })
95
+ ```
96
+
97
+ ### Testing Controllers
98
+
99
+ Controllers in Navios should use endpoints defined with `@navios/builder` for proper type safety and schema validation.
100
+
101
+ #### Endpoint Definitions
102
+
103
+ ```typescript
104
+ // api/user.endpoints.ts
105
+ import { builder } from '@navios/builder'
106
+
107
+ import { z } from 'zod'
108
+
109
+ const userApi = builder()
110
+
111
+ export const getUserByIdEndpoint = userApi.declareEndpoint({
112
+ method: 'GET',
113
+ url: '/users/$id',
114
+ responseSchema: z.object({
115
+ id: z.string(),
116
+ name: z.string(),
117
+ email: z.string().email(),
118
+ }),
119
+ })
120
+
121
+ export const createUserEndpoint = userApi.declareEndpoint({
122
+ method: 'POST',
123
+ url: '/users',
124
+ requestSchema: z.object({
125
+ name: z.string().min(1),
126
+ email: z.string().email(),
127
+ }),
128
+ responseSchema: z.object({
129
+ id: z.string(),
130
+ name: z.string(),
131
+ email: z.string().email(),
132
+ }),
133
+ })
134
+ ```
135
+
136
+ #### Controller Implementation
137
+
138
+ ```typescript
139
+ // user.controller.ts
140
+ import { Controller, Endpoint, EndpointParams } from '@navios/core'
141
+ import { inject } from '@navios/di'
142
+
143
+ import { createUserEndpoint, getUserByIdEndpoint } from '../api/user.endpoints'
144
+ import { UserService } from './user.service'
145
+
146
+ @Controller()
147
+ export class UserController {
148
+ private userService = inject(UserService)
149
+
150
+ @Endpoint(getUserByIdEndpoint)
151
+ async getUserById(params: EndpointParams<typeof getUserByIdEndpoint>) {
152
+ const user = await this.userService.findById(params.params.id)
153
+ if (!user) {
154
+ throw new NotFoundException('User not found')
155
+ }
156
+ return user
157
+ }
158
+
159
+ @Endpoint(createUserEndpoint)
160
+ async createUser(params: EndpointParams<typeof createUserEndpoint>) {
161
+ return this.userService.create(params.data)
162
+ }
163
+ }
164
+ ```
165
+
166
+ #### Testing Controllers
167
+
168
+ ```typescript
169
+ import type { EndpointParams } from '@navios/core'
170
+
171
+ import { NotFoundException } from '@navios/core'
172
+ import { TestContainer } from '@navios/core/testing'
173
+
174
+ import { UserController } from './user.controller'
175
+ import { UserService } from './user.service'
176
+
177
+ describe('UserController', () => {
178
+ let container: TestContainer
179
+ let controller: UserController
180
+ let userService: vi.Mocked<UserService>
181
+
182
+ beforeEach(async () => {
183
+ userService = {
184
+ findById: vi.fn(),
185
+ findAll: vi.fn(),
186
+ create: vi.fn(),
187
+ update: vi.fn(),
188
+ delete: vi.fn(),
189
+ } as any
190
+
191
+ container = new TestContainer()
192
+ // Use dependency injection or manual injection
193
+ controller = await container.get(UserController)
194
+ // Inject mocked service (implementation depends on your DI setup)
195
+ })
196
+
197
+ describe('getUserById', () => {
198
+ it('should return user when found', async () => {
199
+ const mockUser = { id: '1', name: 'John', email: 'john@test.com' }
200
+ userService.findById.mockResolvedValue(mockUser)
201
+
202
+ const result = await controller.getUserById({
203
+ params: { id: '1' },
204
+ query: {},
205
+ data: {},
206
+ headers: {},
207
+ request: {} as any,
208
+ response: {} as any,
209
+ })
210
+
211
+ expect(result).toEqual(mockUser)
212
+ expect(userService.findById).toHaveBeenCalledWith('1')
213
+ })
214
+
215
+ it('should throw NotFoundException when user not found', async () => {
216
+ userService.findById.mockResolvedValue(null)
217
+
218
+ await expect(
219
+ controller.getUserById({
220
+ params: { id: '1' },
221
+ query: {},
222
+ data: {},
223
+ headers: {},
224
+ request: {} as any,
225
+ response: {} as any,
226
+ }),
227
+ ).rejects.toThrow(NotFoundException)
228
+ })
229
+ })
230
+
231
+ describe('createUser', () => {
232
+ it('should create and return new user', async () => {
233
+ const userData = { name: 'John', email: 'john@test.com' }
234
+ const createdUser = { id: '1', ...userData }
235
+
236
+ userService.create.mockResolvedValue(createdUser)
237
+
238
+ const result = await controller.createUser({
239
+ params: {},
240
+ query: {},
241
+ data: userData,
242
+ headers: {},
243
+ request: {} as any,
244
+ response: {} as any,
245
+ })
246
+
247
+ expect(result).toEqual(createdUser)
248
+ expect(userService.create).toHaveBeenCalledWith(userData)
249
+ })
250
+ })
251
+ })
252
+ ```
253
+
254
+ ### Testing Guards
255
+
256
+ ```typescript
257
+ import { TestContainer } from '@navios/core/testing'
258
+
259
+ import { AuthGuard } from './auth.guard'
260
+ import { JwtService } from './jwt.service'
261
+ import { UserService } from './user.service'
262
+
263
+ describe('AuthGuard', () => {
264
+ let guard: AuthGuard
265
+ let jwtService: vi.Mocked<JwtService>
266
+ let userService: vi.Mocked<UserService>
267
+
268
+ beforeEach(() => {
269
+ const container = new TestContainer()
270
+
271
+ jwtService = {
272
+ verify: vi.fn(),
273
+ } as any
274
+
275
+ userService = {
276
+ findById: vi.fn(),
277
+ } as any
278
+
279
+ container.bind(JwtService).toValue(jwtService)
280
+ container.bind(UserService).toValue(userService)
281
+
282
+ guard = container.get(AuthGuard)
283
+ })
284
+
285
+ describe('canActivate', () => {
286
+ it('should return true for valid token', async () => {
287
+ const mockUser = { id: '1', isActive: true }
288
+ const context = {
289
+ headers: { authorization: 'Bearer valid-token' },
290
+ request: {},
291
+ } as any
292
+
293
+ jwtService.verify.mockResolvedValue({ sub: '1' })
294
+ userService.findById.mockResolvedValue(mockUser)
295
+
296
+ const result = await guard.canActivate(context)
297
+
298
+ expect(result).toBe(true)
299
+ expect(context.request.user).toEqual(mockUser)
300
+ })
301
+
302
+ it('should return false for invalid token', async () => {
303
+ const context = {
304
+ headers: { authorization: 'Bearer invalid-token' },
305
+ request: {},
306
+ } as any
307
+
308
+ jwtService.verify.mockRejectedValue(new Error('Invalid token'))
309
+
310
+ const result = await guard.canActivate(context)
311
+
312
+ expect(result).toBe(false)
313
+ expect(context.request.user).toBeUndefined()
314
+ })
315
+
316
+ it('should return false when no token provided', async () => {
317
+ const context = {
318
+ headers: {},
319
+ request: {},
320
+ } as any
321
+
322
+ const result = await guard.canActivate(context)
323
+
324
+ expect(result).toBe(false)
325
+ })
326
+ })
327
+ })
328
+ ```
329
+
330
+ ## Integration Testing
331
+
332
+ ### Testing Module Integration
333
+
334
+ ```typescript
335
+ import { NaviosFactory } from '@navios/core'
336
+
337
+ import { DatabaseService } from './database.service'
338
+ import { UserModule } from './user.module'
339
+ import { UserService } from './user.service'
340
+
341
+ describe('UserModule', () => {
342
+ let module: TestingModule
343
+ let userService: UserService
344
+ let databaseService: DatabaseService
345
+
346
+ beforeAll(async () => {
347
+ module = await NaviosFactory.create(UserModule, {
348
+ adapter: defineFastifyEnvironment(),
349
+ })
350
+ await module.init()
351
+
352
+ userService = module.get<UserService>(UserService)
353
+ databaseService = module.get<DatabaseService>(DatabaseService)
354
+ })
355
+
356
+ afterAll(async () => {
357
+ await module.close()
358
+ })
359
+
360
+ it('should be defined', () => {
361
+ expect(userService).toBeDefined()
362
+ expect(databaseService).toBeDefined()
363
+ })
364
+
365
+ it('should inject dependencies correctly', () => {
366
+ expect(userService).toBeInstanceOf(UserService)
367
+ })
368
+ })
369
+ ```
370
+
371
+ ### Testing HTTP Endpoints
372
+
373
+ When testing HTTP endpoints, ensure your endpoints are defined using `@navios/builder` for proper validation and type safety.
374
+
375
+ ```typescript
376
+ import { defineFastifyEnvironment } from '@navios/adapter-fastify'
377
+ import { NaviosFactory } from '@navios/core'
378
+
379
+ import * as request from 'supertest'
380
+
381
+ import { AppModule } from '../app.module'
382
+
383
+ describe('UserController (e2e)', () => {
384
+ let app: any
385
+ let httpServer: any
386
+
387
+ beforeAll(async () => {
388
+ const moduleFixture = await NaviosFactory.create(AppModule, {
389
+ adapter: defineFastifyEnvironment(),
390
+ })
391
+ await moduleFixture.init()
392
+
393
+ await app.init()
394
+ httpServer = app.getServer()
395
+ })
396
+
397
+ afterAll(async () => {
398
+ await app.close()
399
+ })
400
+
401
+ describe('/users (GET)', () => {
402
+ it('should return array of users', () => {
403
+ return request(httpServer)
404
+ .get('/users')
405
+ .expect(200)
406
+ .expect((res) => {
407
+ expect(Array.isArray(res.body)).toBe(true)
408
+ })
409
+ })
410
+ })
411
+
412
+ describe('/users/:id (GET)', () => {
413
+ it('should return user by id', () => {
414
+ return request(httpServer)
415
+ .get('/users/1')
416
+ .expect(200)
417
+ .expect((res) => {
418
+ expect(res.body).toHaveProperty('id')
419
+ expect(res.body.id).toBe('1')
420
+ })
421
+ })
422
+
423
+ it('should return 404 for non-existent user', () => {
424
+ return request(httpServer).get('/users/999').expect(404)
425
+ })
426
+ })
427
+
428
+ describe('/users (POST)', () => {
429
+ it('should create new user', () => {
430
+ const userData = {
431
+ name: 'John Doe',
432
+ email: 'john@test.com',
433
+ }
434
+
435
+ return request(httpServer)
436
+ .post('/users')
437
+ .send(userData)
438
+ .expect(201)
439
+ .expect((res) => {
440
+ expect(res.body).toHaveProperty('id')
441
+ expect(res.body.name).toBe(userData.name)
442
+ expect(res.body.email).toBe(userData.email)
443
+ })
444
+ })
445
+
446
+ it('should return 400 for invalid data', () => {
447
+ return request(httpServer)
448
+ .post('/users')
449
+ .send({ name: '' }) // Invalid data
450
+ .expect(400)
451
+ })
452
+ })
453
+ })
454
+
455
+ describe('/upload (POST) - Multipart', () => {
456
+ it('should handle file uploads', () => {
457
+ return request(httpServer)
458
+ .post('/upload')
459
+ .attach('file', Buffer.from('test file content'), 'test.txt')
460
+ .field('description', 'Test file upload')
461
+ .expect(201)
462
+ .expect((res) => {
463
+ expect(res.body).toHaveProperty('filename')
464
+ expect(res.body).toHaveProperty('size')
465
+ })
466
+ })
467
+ })
468
+ })
469
+ ```
470
+
471
+ ### Testing Multipart Endpoints
472
+
473
+ When testing multipart endpoints defined with `@navios/builder`:
474
+
475
+ ```typescript
476
+ // api/upload.endpoints.ts
477
+ import { builder } from '@navios/builder'
478
+ // upload.controller.ts
479
+ import { Controller, Multipart, MultipartParams } from '@navios/core'
480
+
481
+ import { z } from 'zod'
482
+
483
+ const api = builder()
484
+
485
+ export const uploadEndpoint = api.declareMultipart({
486
+ method: 'POST',
487
+ url: '/upload',
488
+ requestSchema: z.object({
489
+ files: z.array(z.instanceof(File)),
490
+ description: z.string(),
491
+ }),
492
+ responseSchema: z.object({
493
+ filename: z.string(),
494
+ size: z.number(),
495
+ }),
496
+ })
497
+
498
+ @Controller()
499
+ export class UploadController {
500
+ @Multipart(uploadEndpoint)
501
+ async uploadFile(params: MultipartParams<typeof uploadEndpoint>) {
502
+ const { files, description } = params.data
503
+ // Handle file upload logic
504
+ return {
505
+ filename: files[0].name,
506
+ size: files[0].size,
507
+ }
508
+ }
509
+ }
510
+ ```
511
+
512
+ ## Testing with Authentication
513
+
514
+ ### Testing Protected Endpoints
515
+
516
+ ```typescript
517
+ describe('Protected Endpoints', () => {
518
+ let authToken: string
519
+
520
+ beforeAll(async () => {
521
+ // Login and get auth token
522
+ const loginResponse = await request(httpServer)
523
+ .post('/auth/login')
524
+ .send({
525
+ email: 'test@example.com',
526
+ password: 'password',
527
+ })
528
+ .expect(200)
529
+
530
+ authToken = loginResponse.body.token
531
+ })
532
+
533
+ describe('/profile (GET)', () => {
534
+ it('should return user profile with valid token', () => {
535
+ return request(httpServer)
536
+ .get('/profile')
537
+ .set('Authorization', `Bearer ${authToken}`)
538
+ .expect(200)
539
+ })
540
+
541
+ it('should return 401 without token', () => {
542
+ return request(httpServer).get('/profile').expect(401)
543
+ })
544
+
545
+ it('should return 401 with invalid token', () => {
546
+ return request(httpServer)
547
+ .get('/profile')
548
+ .set('Authorization', `Bearer invalid-token`)
549
+ .expect(401)
550
+ })
551
+ })
552
+ })
553
+ ```
554
+
555
+ ## Mock Strategies
556
+
557
+ ### Creating Service Mocks
558
+
559
+ ```typescript
560
+ // test/mocks/user.service.mock.ts
561
+ export const mockUserService = {
562
+ findById: vi.fn(),
563
+ findAll: vi.fn(),
564
+ create: vi.fn(),
565
+ update: vi.fn(),
566
+ delete: vi.fn(),
567
+ }
568
+
569
+ // Reset mocks between tests
570
+ beforeEach(() => {
571
+ vi.clearAllMocks()
572
+ })
573
+ ```
574
+
575
+ ### Factory Pattern for Test Data
576
+
577
+ ```typescript
578
+ // test/factories/user.factory.ts
579
+ export class UserFactory {
580
+ static create(overrides: Partial<User> = {}): User {
581
+ return {
582
+ id: '1',
583
+ name: 'John Doe',
584
+ email: 'john@test.com',
585
+ createdAt: new Date(),
586
+ updatedAt: new Date(),
587
+ ...overrides,
588
+ }
589
+ }
590
+
591
+ static createMany(count: number, overrides: Partial<User> = {}): User[] {
592
+ return Array.from({ length: count }, (_, i) =>
593
+ this.create({
594
+ id: String(i + 1),
595
+ ...overrides,
596
+ }),
597
+ )
598
+ }
599
+ }
600
+
601
+ // Usage in tests
602
+ describe('UserService', () => {
603
+ it('should handle multiple users', async () => {
604
+ const users = UserFactory.createMany(3)
605
+ mockDatabase.users.findMany.mockResolvedValue(users)
606
+
607
+ const result = await userService.findAll()
608
+
609
+ expect(result).toHaveLength(3)
610
+ expect(result).toEqual(users)
611
+ })
612
+ })
613
+ ```
614
+
615
+ ## Test Organization
616
+
617
+ ### Folder Structure
618
+
619
+ ```
620
+ src/
621
+ ├── user/
622
+ │ ├── user.controller.ts
623
+ │ ├── user.service.ts
624
+ │ ├── user.module.ts
625
+ │ └── __tests__/
626
+ │ ├── user.controller.spec.ts
627
+ │ ├── user.service.spec.ts
628
+ └── test/
629
+ ├── fixtures/
630
+ ├── mocks/
631
+ ├── factories/
632
+ └── e2e/
633
+ └── user.e2e-spec.ts
634
+ ```
635
+
636
+ ## Best Practices
637
+
638
+ ### 1. Test Isolation
639
+
640
+ Ensure tests don't depend on each other:
641
+
642
+ ```typescript
643
+ describe('UserService', () => {
644
+ beforeEach(() => {
645
+ // Reset state before each test
646
+ vi.clearAllMocks()
647
+ })
648
+
649
+ // Each test should be independent
650
+ })
651
+ ```
652
+
653
+ ### 2. Use Descriptive Test Names
654
+
655
+ ```typescript
656
+ // ✅ Good - Descriptive names
657
+ it('should throw NotFoundException when user does not exist', () => {})
658
+ it('should return user data when valid ID is provided', () => {})
659
+
660
+ // ❌ Avoid - Vague names
661
+ it('should work', () => {})
662
+ it('should fail', () => {})
663
+ ```
664
+
665
+ ### 3. Test Edge Cases
666
+
667
+ ```typescript
668
+ describe('UserService.findById', () => {
669
+ it('should return user for valid ID', async () => {
670
+ // Happy path
671
+ })
672
+
673
+ it('should return null for non-existent ID', async () => {
674
+ // Edge case
675
+ })
676
+
677
+ it('should handle database connection errors', async () => {
678
+ // Error case
679
+ })
680
+
681
+ it('should validate ID format', async () => {
682
+ // Input validation
683
+ })
684
+ })
685
+ ```
686
+
687
+ ### 4. Keep Tests Simple
688
+
689
+ ```typescript
690
+ // ✅ Good - Simple and focused
691
+ it('should create user with valid data', async () => {
692
+ const userData = { name: 'John', email: 'john@test.com' }
693
+ mockDatabase.users.create.mockResolvedValue({ id: '1', ...userData })
694
+
695
+ const result = await userService.create(userData)
696
+
697
+ expect(result).toMatchObject(userData)
698
+ })
699
+
700
+ // ❌ Avoid - Testing multiple things
701
+ it('should create user and send email and log event', async () => {
702
+ // Too many responsibilities
703
+ })
704
+ ```