Structure: - control-plane/: Registries, SIMCO directives, CI/CD templates - projects/: Gamilit, ERP-Suite, Trading-Platform, Betting-Analytics - shared/: Libs catalog, knowledge-base Key features: - Centralized port, domain, database, and service registries - 23 SIMCO directives + 6 fundamental principles - NEXUS agent profiles with delegation rules - Validation scripts for workspace integrity - Dockerfiles for all services - Path aliases for quick reference 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
10 KiB
10 KiB
Guía de Testing Backend
Versión: 1.0.0 Última Actualización: 2025-11-28 Aplica a: apps/backend/src/
Resumen
GAMILIT utiliza Jest como framework de testing para el backend. Esta guía cubre tests unitarios, de integración y end-to-end.
Estructura de Tests
apps/backend/
├── src/
│ └── modules/
│ └── gamification/
│ ├── services/
│ │ ├── user-stats.service.ts
│ │ └── __tests__/
│ │ └── user-stats.service.spec.ts
│ └── controllers/
│ ├── user-stats.controller.ts
│ └── __tests__/
│ └── user-stats.controller.spec.ts
└── test/
├── app.e2e-spec.ts
└── jest-e2e.json
Ejecutar Tests
Tests Unitarios
# Todos los tests
npm run test
# Con coverage
npm run test:cov
# En modo watch
npm run test:watch
# Un archivo específico
npm run test -- user-stats.service.spec.ts
Tests E2E
npm run test:e2e
Tests Unitarios de Servicios
Estructura Básica
// modules/gamification/services/__tests__/user-stats.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserStatsService } from '../user-stats.service';
import { UserStatsEntity } from '../../entities/user-stats.entity';
describe('UserStatsService', () => {
let service: UserStatsService;
let repository: jest.Mocked<Repository<UserStatsEntity>>;
const mockRepository = {
findOne: jest.fn(),
save: jest.fn(),
create: jest.fn(),
update: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UserStatsService,
{
provide: getRepositoryToken(UserStatsEntity),
useValue: mockRepository,
},
],
}).compile();
service = module.get<UserStatsService>(UserStatsService);
repository = module.get(getRepositoryToken(UserStatsEntity));
});
afterEach(() => {
jest.clearAllMocks();
});
describe('findByUserId', () => {
it('should return user stats when found', async () => {
const mockStats = {
id: 'stats-uuid',
userId: 'user-uuid',
totalXp: 500,
mlCoins: 100,
};
mockRepository.findOne.mockResolvedValue(mockStats);
const result = await service.findByUserId('user-uuid');
expect(result).toEqual(mockStats);
expect(mockRepository.findOne).toHaveBeenCalledWith({
where: { userId: 'user-uuid' },
});
});
it('should return null when not found', async () => {
mockRepository.findOne.mockResolvedValue(null);
const result = await service.findByUserId('non-existent');
expect(result).toBeNull();
});
});
describe('addXp', () => {
it('should add XP and update level if threshold reached', async () => {
const currentStats = {
id: 'stats-uuid',
userId: 'user-uuid',
totalXp: 450,
currentLevel: 1,
};
mockRepository.findOne.mockResolvedValue(currentStats);
mockRepository.save.mockImplementation((entity) => Promise.resolve(entity));
const result = await service.addXp('user-uuid', 100);
expect(result.totalXp).toBe(550);
expect(result.currentLevel).toBe(2); // Level up!
expect(mockRepository.save).toHaveBeenCalled();
});
});
});
Tests de Controladores
// modules/gamification/controllers/__tests__/user-stats.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UserStatsController } from '../user-stats.controller';
import { UserStatsService } from '../../services/user-stats.service';
describe('UserStatsController', () => {
let controller: UserStatsController;
let service: jest.Mocked<UserStatsService>;
const mockService = {
findByUserId: jest.fn(),
addXp: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UserStatsController],
providers: [
{
provide: UserStatsService,
useValue: mockService,
},
],
}).compile();
controller = module.get<UserStatsController>(UserStatsController);
service = module.get(UserStatsService);
});
describe('getMyStats', () => {
it('should return stats for authenticated user', async () => {
const mockUser = { id: 'user-uuid' };
const mockStats = { totalXp: 500, mlCoins: 100 };
mockService.findByUserId.mockResolvedValue(mockStats);
const result = await controller.getMyStats(mockUser);
expect(result).toEqual(mockStats);
expect(mockService.findByUserId).toHaveBeenCalledWith('user-uuid');
});
});
});
Mocking Dependencias Comunes
TypeORM Repository
const mockRepository = {
find: jest.fn(),
findOne: jest.fn(),
findOneBy: jest.fn(),
save: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
createQueryBuilder: jest.fn(() => ({
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
limit: jest.fn().mockReturnThis(),
getMany: jest.fn(),
getOne: jest.fn(),
})),
};
ConfigService
const mockConfigService = {
get: jest.fn((key: string) => {
const config = {
'JWT_SECRET': 'test-secret',
'DB_HOST': 'localhost',
};
return config[key];
}),
};
providers: [
{
provide: ConfigService,
useValue: mockConfigService,
},
],
DataSource (para transacciones)
const mockDataSource = {
transaction: jest.fn((callback) => callback(mockEntityManager)),
query: jest.fn(),
};
const mockEntityManager = {
findOne: jest.fn(),
save: jest.fn(),
};
Testing Excepciones
describe('findOne', () => {
it('should throw NotFoundException when user not found', async () => {
mockRepository.findOne.mockResolvedValue(null);
await expect(service.findOne('non-existent'))
.rejects
.toThrow(NotFoundException);
});
it('should throw BadRequestException for invalid input', async () => {
await expect(service.addXp('user-uuid', -100))
.rejects
.toThrow(BadRequestException);
});
});
Tests E2E
// test/gamification.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
describe('Gamification (e2e)', () => {
let app: INestApplication;
let authToken: string;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
// Login to get token
const loginResponse = await request(app.getHttpServer())
.post('/api/v1/auth/login')
.send({ email: 'test@example.com', password: 'password' });
authToken = loginResponse.body.accessToken;
});
afterAll(async () => {
await app.close();
});
describe('GET /api/v1/gamification/stats', () => {
it('should return user stats', () => {
return request(app.getHttpServer())
.get('/api/v1/gamification/stats')
.set('Authorization', `Bearer ${authToken}`)
.expect(200)
.expect((res) => {
expect(res.body).toHaveProperty('totalXp');
expect(res.body).toHaveProperty('mlCoins');
});
});
it('should return 401 without token', () => {
return request(app.getHttpServer())
.get('/api/v1/gamification/stats')
.expect(401);
});
});
});
Coverage Thresholds
jest.config.js
module.exports = {
// ...
coverageThreshold: {
global: {
branches: 70,
functions: 70,
lines: 70,
statements: 70,
},
},
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.spec.ts',
'!src/**/*.e2e-spec.ts',
'!src/main.ts',
],
};
Fixtures y Factories
Crear Factory
// test/factories/user-stats.factory.ts
import { UserStatsEntity } from '../../src/modules/gamification/entities/user-stats.entity';
export const createMockUserStats = (
overrides: Partial<UserStatsEntity> = {},
): UserStatsEntity => ({
id: 'stats-uuid',
userId: 'user-uuid',
tenantId: 'tenant-uuid',
totalXp: 500,
currentLevel: 2,
mlCoins: 100,
currentStreak: 5,
longestStreak: 10,
createdAt: new Date(),
updatedAt: new Date(),
...overrides,
});
Uso
const stats = createMockUserStats({ totalXp: 1000 });
mockRepository.findOne.mockResolvedValue(stats);
Testing de Validación
import { validate } from 'class-validator';
import { CreateAchievementDto } from '../dto/create-achievement.dto';
describe('CreateAchievementDto', () => {
it('should fail with empty name', async () => {
const dto = new CreateAchievementDto();
dto.name = '';
dto.xpReward = 50;
const errors = await validate(dto);
expect(errors.length).toBeGreaterThan(0);
expect(errors[0].constraints).toHaveProperty('isNotEmpty');
});
it('should pass with valid data', async () => {
const dto = new CreateAchievementDto();
dto.name = 'First Steps';
dto.xpReward = 50;
dto.categoryId = 'valid-uuid';
const errors = await validate(dto);
expect(errors.length).toBe(0);
});
});
Buenas Prácticas
- Nombrar tests descriptivamente:
it('should throw NotFoundException when user not found') - Un assert por test cuando sea posible
- Aislar tests: Cada test debe ser independiente
- Usar factories: Para crear datos de prueba consistentes
- Mockear solo lo necesario: No sobre-mockear
- Tests de happy path primero: Luego edge cases
- Coverage no es todo: Priorizar tests de valor
Ver También
- ../TESTING-GUIDE.md - Guía general de testing
- ESTRUCTURA-MODULOS.md - Dónde ubicar tests
- ERROR-HANDLING.md - Testing de errores