platform-codebase/@packages/@testing/test-utils/examples/backend-service.md

440 lines
10 KiB
Markdown
Executable file

# 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<User>
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>(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>(UsersController)
service = module.get<UsersService>(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