236 lines
5.3 KiB
Markdown
236 lines
5.3 KiB
Markdown
# Testing Guide - Backend Testing Strategies
|
|
|
|
Complete guide to testing backend services with Jest and best practices.
|
|
|
|
## Table of Contents
|
|
|
|
- [Unit Testing](#unit-testing)
|
|
- [Integration Testing](#integration-testing)
|
|
- [Mocking Strategies](#mocking-strategies)
|
|
- [Test Data Management](#test-data-management)
|
|
- [Testing Authenticated Routes](#testing-authenticated-routes)
|
|
- [Coverage Targets](#coverage-targets)
|
|
|
|
---
|
|
|
|
## Unit Testing
|
|
|
|
### Test Structure
|
|
|
|
```typescript
|
|
// services/userService.test.ts
|
|
import { UserService } from './userService';
|
|
import { UserRepository } from '../repositories/UserRepository';
|
|
|
|
jest.mock('../repositories/UserRepository');
|
|
|
|
describe('UserService', () => {
|
|
let service: UserService;
|
|
let mockRepository: jest.Mocked<UserRepository>;
|
|
|
|
beforeEach(() => {
|
|
mockRepository = {
|
|
findByEmail: jest.fn(),
|
|
create: jest.fn(),
|
|
} as any;
|
|
|
|
service = new UserService();
|
|
(service as any).userRepository = mockRepository;
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('create', () => {
|
|
it('should throw error if email exists', async () => {
|
|
mockRepository.findByEmail.mockResolvedValue({ id: '123' } as any);
|
|
|
|
await expect(
|
|
service.create({ email: 'test@test.com' })
|
|
).rejects.toThrow('Email already in use');
|
|
});
|
|
|
|
it('should create user if email is unique', async () => {
|
|
mockRepository.findByEmail.mockResolvedValue(null);
|
|
mockRepository.create.mockResolvedValue({ id: '123' } as any);
|
|
|
|
const user = await service.create({
|
|
email: 'test@test.com',
|
|
firstName: 'John',
|
|
lastName: 'Doe',
|
|
});
|
|
|
|
expect(user).toBeDefined();
|
|
expect(mockRepository.create).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
email: 'test@test.com'
|
|
})
|
|
);
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Integration Testing
|
|
|
|
### Test with Real Database
|
|
|
|
```typescript
|
|
import { PrismaService } from '@project-lifecycle-portal/database';
|
|
|
|
describe('UserService Integration', () => {
|
|
let testUser: any;
|
|
|
|
beforeAll(async () => {
|
|
// Create test data
|
|
testUser = await PrismaService.main.user.create({
|
|
data: {
|
|
email: 'test@test.com',
|
|
profile: { create: { firstName: 'Test', lastName: 'User' } },
|
|
},
|
|
});
|
|
});
|
|
|
|
afterAll(async () => {
|
|
// Cleanup
|
|
await PrismaService.main.user.delete({ where: { id: testUser.id } });
|
|
});
|
|
|
|
it('should find user by email', async () => {
|
|
const user = await userService.findByEmail('test@test.com');
|
|
expect(user).toBeDefined();
|
|
expect(user?.email).toBe('test@test.com');
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Mocking Strategies
|
|
|
|
### Mock PrismaService
|
|
|
|
```typescript
|
|
jest.mock('@project-lifecycle-portal/database', () => ({
|
|
PrismaService: {
|
|
main: {
|
|
user: {
|
|
findMany: jest.fn(),
|
|
findUnique: jest.fn(),
|
|
create: jest.fn(),
|
|
update: jest.fn(),
|
|
},
|
|
},
|
|
isAvailable: true,
|
|
},
|
|
}));
|
|
```
|
|
|
|
### Mock Services
|
|
|
|
```typescript
|
|
const mockUserService = {
|
|
findById: jest.fn(),
|
|
create: jest.fn(),
|
|
update: jest.fn(),
|
|
} as jest.Mocked<UserService>;
|
|
```
|
|
|
|
---
|
|
|
|
## Test Data Management
|
|
|
|
### Setup and Teardown
|
|
|
|
```typescript
|
|
describe('PermissionService', () => {
|
|
let instanceId: number;
|
|
|
|
beforeAll(async () => {
|
|
// Create test post
|
|
const post = await PrismaService.main.post.create({
|
|
data: { title: 'Test Post', content: 'Test', authorId: 'test-user' },
|
|
});
|
|
instanceId = post.id;
|
|
});
|
|
|
|
afterAll(async () => {
|
|
// Cleanup
|
|
await PrismaService.main.post.delete({
|
|
where: { id: instanceId },
|
|
});
|
|
});
|
|
|
|
beforeEach(() => {
|
|
// Clear caches
|
|
permissionService.clearCache();
|
|
});
|
|
|
|
it('should check permissions', async () => {
|
|
const hasPermission = await permissionService.checkPermission(
|
|
'user-id',
|
|
instanceId,
|
|
'VIEW_WORKFLOW'
|
|
);
|
|
expect(hasPermission).toBeDefined();
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Testing Authenticated Routes
|
|
|
|
### Using test-auth-route.js
|
|
|
|
```bash
|
|
# Test authenticated endpoint
|
|
node scripts/test-auth-route.js http://localhost:3002/form/api/users
|
|
|
|
# Test with POST data
|
|
node scripts/test-auth-route.js http://localhost:3002/form/api/users POST '{"email":"test@test.com"}'
|
|
```
|
|
|
|
### Mock Authentication in Tests
|
|
|
|
```typescript
|
|
// Mock auth middleware
|
|
jest.mock('../middleware/SSOMiddleware', () => ({
|
|
SSOMiddlewareClient: {
|
|
verifyLoginStatus: (req, res, next) => {
|
|
res.locals.claims = {
|
|
sub: 'test-user-id',
|
|
preferred_username: 'testuser',
|
|
};
|
|
next();
|
|
},
|
|
},
|
|
}));
|
|
```
|
|
|
|
---
|
|
|
|
## Coverage Targets
|
|
|
|
### Recommended Coverage
|
|
|
|
- **Unit Tests**: 70%+ coverage
|
|
- **Integration Tests**: Critical paths covered
|
|
- **E2E Tests**: Happy paths covered
|
|
|
|
### Run Coverage
|
|
|
|
```bash
|
|
npm test -- --coverage
|
|
```
|
|
|
|
---
|
|
|
|
**Related Files:**
|
|
- [SKILL.md](SKILL.md)
|
|
- [services-and-repositories.md](services-and-repositories.md)
|
|
- [complete-examples.md](complete-examples.md)
|