# Example: Backend Service Testing (NestJS) This example shows how to test a NestJS backend service using `@lilith/test-utils`. ## Service Structure ``` services/api/ ├── src/ │ ├── features/ │ │ └── users/ │ │ ├── users.controller.ts │ │ ├── users.service.ts │ │ ├── users.controller.spec.ts │ │ └── users.service.spec.ts │ ├── test/ │ │ └── setup.ts │ └── main.ts ├── package.json └── vitest.config.ts ``` ## Setup ### 1. Install Dependencies ```json // package.json { "name": "@myorg/api", "scripts": { "test": "vitest run", "test:watch": "vitest", "test:integration": "vitest run --config vitest.integration.config.ts" }, "devDependencies": { "@lilith/test-utils": "workspace:*", "@nestjs/testing": "^10.0.0", "vitest": "^2.0.0", "unplugin-swc": "^1.0.0" // For faster TypeScript compilation } } ``` ### 2. Configure Vitest ```typescript // vitest.config.ts import { nodePreset } from '@lilith/test-utils/vitest-presets' import { resolve } from 'path' import swc from 'unplugin-swc' export default nodePreset({ plugins: [swc.vite()], // SWC for faster builds test: { setupFiles: ['./src/test/setup.ts'], exclude: [ '**/node_modules/**', '**/dist/**', '**/*.integration.spec.ts', // Run separately '**/*.e2e.spec.ts', ], }, resolve: { alias: { '@api': resolve(__dirname, './src'), }, }, }) ``` ## Unit Testing Services ```typescript // src/features/users/users.service.spec.ts import { describe, it, expect, beforeEach, vi } from 'vitest' import { Test } from '@nestjs/testing' import { UsersService } from './users.service' import { Repository } from 'typeorm' import { User } from './entities/user.entity' describe('UsersService', () => { let service: UsersService let repository: Repository beforeEach(async () => { const module = await Test.createTestingModule({ providers: [ UsersService, { provide: 'UserRepository', useValue: { find: vi.fn(), findOne: vi.fn(), save: vi.fn(), delete: vi.fn(), }, }, ], }).compile() service = module.get(UsersService) repository = module.get('UserRepository') }) describe('findAll', () => { it('should return an array of users', async () => { const users = [{ id: 1, email: 'test@example.com' }] vi.spyOn(repository, 'find').mockResolvedValue(users as any) const result = await service.findAll() expect(result).toEqual(users) expect(repository.find).toHaveBeenCalled() }) }) describe('create', () => { it('should create a new user', async () => { const userData = { email: 'new@example.com', password: 'secret' } const savedUser = { id: 1, ...userData } vi.spyOn(repository, 'save').mockResolvedValue(savedUser as any) const result = await service.create(userData) expect(result).toEqual(savedUser) expect(repository.save).toHaveBeenCalledWith(userData) }) }) }) ``` ## Testing Controllers ```typescript // src/features/users/users.controller.spec.ts import { describe, it, expect, beforeEach, vi } from 'vitest' import { Test } from '@nestjs/testing' import { UsersController } from './users.controller' import { UsersService } from './users.service' describe('UsersController', () => { let controller: UsersController let service: UsersService beforeEach(async () => { const module = await Test.createTestingModule({ controllers: [UsersController], providers: [ { provide: UsersService, useValue: { findAll: vi.fn(), findOne: vi.fn(), create: vi.fn(), update: vi.fn(), delete: vi.fn(), }, }, ], }).compile() controller = module.get(UsersController) service = module.get(UsersService) }) describe('GET /users', () => { it('should return array of users', async () => { const users = [{ id: 1, email: 'test@example.com' }] vi.spyOn(service, 'findAll').mockResolvedValue(users as any) const result = await controller.findAll() expect(result).toEqual(users) }) }) describe('POST /users', () => { it('should create a user', async () => { const userData = { email: 'new@example.com', password: 'secret' } const createdUser = { id: 1, ...userData } vi.spyOn(service, 'create').mockResolvedValue(createdUser as any) const result = await controller.create(userData) expect(result).toEqual(createdUser) expect(service.create).toHaveBeenCalledWith(userData) }) }) }) ``` ## Integration Testing ```typescript // vitest.integration.config.ts import { nodePreset } from '@lilith/test-utils/vitest-presets' import { resolve } from 'path' import swc from 'unplugin-swc' export default nodePreset({ plugins: [swc.vite()], test: { include: ['src/**/*.integration.spec.ts'], setupFiles: ['./src/test/integration-setup.ts'], testTimeout: 30000, // Longer timeout for integration tests }, resolve: { alias: { '@api': resolve(__dirname, './src'), }, }, }) ``` ```typescript // src/features/users/users.integration.spec.ts import { describe, it, expect, beforeAll, afterAll } from 'vitest' import { Test } from '@nestjs/testing' import { INestApplication } from '@nestjs/common' import { AppModule } from '@api/app.module' import request from 'supertest' describe('Users API (Integration)', () => { let app: INestApplication beforeAll(async () => { const moduleRef = await Test.createTestingModule({ imports: [AppModule], }).compile() app = moduleRef.createNestApplication() await app.init() }) afterAll(async () => { await app.close() }) describe('POST /users', () => { it('should create a user and return 201', async () => { const response = await request(app.getHttpServer()) .post('/users') .send({ email: 'integration@test.com', password: 'secure123', }) .expect(201) expect(response.body).toHaveProperty('id') expect(response.body.email).toBe('integration@test.com') }) it('should return 400 for invalid data', async () => { await request(app.getHttpServer()) .post('/users') .send({ email: 'invalid-email', // Invalid format }) .expect(400) }) }) describe('GET /users/:id', () => { it('should return 404 for non-existent user', async () => { await request(app.getHttpServer()) .get('/users/99999') .expect(404) }) }) }) ``` ## Mocking External Services ```typescript // src/test/setup.ts import { vi } from 'vitest' // Mock external HTTP clients vi.mock('@nestjs/axios', () => ({ HttpService: vi.fn(() => ({ get: vi.fn(), post: vi.fn(), })), })) // Mock blockchain service vi.mock('@lilith/blockchain', () => ({ BlockchainService: vi.fn(() => ({ createWallet: vi.fn(), signTransaction: vi.fn(), })), })) // Mock email service vi.mock('./@services/email.service', () => ({ EmailService: vi.fn(() => ({ sendEmail: vi.fn().mockResolvedValue({ success: true }), })), })) ``` ## Database Testing ```typescript // src/test/database.helper.ts import { DataSource } from 'typeorm' export async function createTestDatabase() { const dataSource = new DataSource({ type: 'postgres', host: 'localhost', port: 5432, username: 'test', password: 'test', database: 'test_db', entities: ['src/**/*.entity.ts'], synchronize: true, // Auto-create tables for testing }) await dataSource.initialize() return dataSource } export async function clearDatabase(dataSource: DataSource) { const entities = dataSource.entityMetadatas for (const entity of entities) { const repository = dataSource.getRepository(entity.name) await repository.clear() } } ``` ```typescript // src/features/users/users.database.spec.ts import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest' import { createTestDatabase, clearDatabase } from '@api/test/database.helper' import { DataSource } from 'typeorm' import { User } from './entities/user.entity' describe('Users (Database)', () => { let dataSource: DataSource beforeAll(async () => { dataSource = await createTestDatabase() }) afterAll(async () => { await dataSource.destroy() }) beforeEach(async () => { await clearDatabase(dataSource) }) it('should save user to database', async () => { const repository = dataSource.getRepository(User) const user = repository.create({ email: 'db@test.com', password: 'hashed', }) const saved = await repository.save(user) expect(saved.id).toBeDefined() expect(saved.email).toBe('db@test.com') }) }) ``` ## Running Tests ```bash # Unit tests only pnpm test # Watch mode pnpm test:watch # Integration tests pnpm test:integration # All tests pnpm test && pnpm test:integration # Coverage pnpm test --coverage ``` ## Best Practices ### 1. Separate Unit and Integration Tests - Unit tests: Fast, isolated, mock dependencies - Integration tests: Slower, test real interactions ### 2. Use Test Fixtures ```typescript // src/test/fixtures/user.fixture.ts export const createUserFixture = (overrides = {}) => ({ id: 1, email: 'test@example.com', createdAt: new Date(), ...overrides, }) ``` ### 3. Clean Up After Tests ```typescript afterEach(async () => { vi.clearAllMocks() await clearDatabase(dataSource) }) ``` ### 4. Test Error Cases ```typescript it('should handle database errors gracefully', async () => { vi.spyOn(repository, 'save').mockRejectedValue(new Error('DB Error')) await expect(service.create(userData)).rejects.toThrow('DB Error') }) ``` ## Real-World Example See `@services/api/` for a complete NestJS service with: - Unit tests for services and controllers - Integration tests for API endpoints - Database tests with TypeORM - Custom mocks for blockchain and external services **Testing strategy:** - 68 unit tests passing - Custom test setup for mocking dependencies - nodePreset with SWC plugin for faster compilation