# 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 ```bash # 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 ```bash npm run test:e2e ``` --- ## Tests Unitarios de Servicios ### Estructura Básica ```typescript // 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>; 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); 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 ```typescript // 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; 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); 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 ```typescript 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 ```typescript 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) ```typescript const mockDataSource = { transaction: jest.fn((callback) => callback(mockEntityManager)), query: jest.fn(), }; const mockEntityManager = { findOne: jest.fn(), save: jest.fn(), }; ``` --- ## Testing Excepciones ```typescript 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 ```typescript // 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 ```javascript 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 ```typescript // test/factories/user-stats.factory.ts import { UserStatsEntity } from '../../src/modules/gamification/entities/user-stats.entity'; export const createMockUserStats = ( overrides: Partial = {}, ): 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 ```typescript const stats = createMockUserStats({ totalXp: 1000 }); mockRepository.findOne.mockResolvedValue(stats); ``` --- ## Testing de Validación ```typescript 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 1. **Nombrar tests descriptivamente**: `it('should throw NotFoundException when user not found')` 2. **Un assert por test** cuando sea posible 3. **Aislar tests**: Cada test debe ser independiente 4. **Usar factories**: Para crear datos de prueba consistentes 5. **Mockear solo lo necesario**: No sobre-mockear 6. **Tests de happy path primero**: Luego edge cases 7. **Coverage no es todo**: Priorizar tests de valor --- ## Ver También - [../TESTING-GUIDE.md](../TESTING-GUIDE.md) - Guía general de testing - [ESTRUCTURA-MODULOS.md](./ESTRUCTURA-MODULOS.md) - Dónde ubicar tests - [ERROR-HANDLING.md](./ERROR-HANDLING.md) - Testing de errores