# 🧪 Testing y Mocks para Datos Médicos ## 🎯 Objetivo Explicar qué son los mocks para datos médicos y establecer estrategia de testing con cobertura mínima. --- ## 🤔 ¿Qué son los Mocks? ### Definición **Mock (Simulación):** Objeto que imita el comportamiento de un objeto real para propósitos de testing, sin ejecutar la funcionalidad real. ### ¿Por qué usar Mocks? 1. **Aislamiento:** Probar una función sin depender de otras 2. **Velocidad:** Evitar llamadas lentas (BD, APIs) 3. **Control:** Controlar el comportamiento de dependencias 4. **Reproducibilidad:** Tests siempre dan el mismo resultado --- ## 📋 Ejemplos de Mocks para Datos Médicos ### Mock 1: Repository Mock **Problema:** No queremos acceder a la BD real en tests **Solución:** ```typescript // tests/mocks/DrugRepositoryMock.ts export class DrugRepositoryMock implements IDrugRepository { private drugs: Map = new Map(); async findById(id: string): Promise { return this.drugs.get(id) || null; } async save(drug: Drug): Promise { this.drugs.set(drug.id, drug); return drug; } // Método helper para tests addDrug(drug: Drug): void { this.drugs.set(drug.id, drug); } clear(): void { this.drugs.clear(); } } ``` **Uso en test:** ```typescript describe('DoseValidationService', () => { let mockRepository: DrugRepositoryMock; let service: DoseValidationService; beforeEach(() => { mockRepository = new DrugRepositoryMock(); service = new DoseValidationService(mockRepository); // Añadir fármaco mock mockRepository.addDrug({ id: 'adrenalina', genericName: 'Adrenalina', adultDose: '1mg IV', routes: ['IV', 'IM'], // ... otros campos }); }); it('should validate dose correctly', async () => { const result = await service.validateDose( 'adrenalina', 0.5, PatientWeight.fromKg(70), PatientAge.fromYears(35), 'IV' ); expect(result.valid).toBe(true); }); }); ``` --- ### Mock 2: Datos Médicos de Prueba **Problema:** Necesitamos datos médicos consistentes para tests **Solución:** ```typescript // tests/fixtures/medicalData.ts export const mockDrugs = { adrenalina: { id: 'adrenalina', genericName: 'Adrenalina', adultDose: '1mg IV/IO cada 3-5 min', pediatricDose: '0.01 mg/kg IV/IO', routes: ['IV', 'IO', 'IM'], category: 'cardiovascular', // ... otros campos }, amiodarona: { id: 'amiodarona', genericName: 'Amiodarona', adultDose: '300mg IV', pediatricDose: '5 mg/kg IV', routes: ['IV'], category: 'cardiovascular', } }; export const mockProtocols = { rcpAdultoSVB: { id: 'rcp-adulto-svb', title: 'RCP Adulto SVB', steps: [ { order: 1, type: 'obligatory', content: 'Garantizar seguridad' }, { order: 2, type: 'obligatory', content: 'Comprobar consciencia' }, { order: 3, type: 'critical', content: 'Iniciar compresiones' }, ], // ... otros campos } }; export const mockDoseRanges = { adrenalinaAdulto: { min: 0.5, max: 1, unit: 'mg', ageGroup: 'adulto' }, adrenalinaPediatrico: { min: 0.01, max: 0.5, unit: 'mg', ageGroup: 'pediatrico' } }; ``` **Uso:** ```typescript import { mockDrugs, mockDoseRanges } from '../fixtures/medicalData'; it('should calculate pediatric dose', async () => { mockRepository.addDrug(mockDrugs.adrenalina); const result = await service.calculatePediatricDose( 'adrenalina', PatientWeight.fromKg(20), PatientAge.fromYears(5) ); expect(result.calculatedDose).toBe(0.2); // 20kg * 0.01 mg/kg }); ``` --- ### Mock 3: Servicios Externos **Problema:** No queremos llamar servicios externos en tests **Solución:** ```typescript // tests/mocks/NotificationServiceMock.ts export class NotificationServiceMock implements INotificationService { public sentNotifications: Array<{ to: string; type: string; data: unknown; }> = []; async notifyReviewers(data: unknown): Promise { this.sentNotifications.push({ to: 'reviewers', type: 'review_request', data }); } async notifyEditor(data: unknown): Promise { this.sentNotifications.push({ to: 'editor', type: 'review_result', data }); } clear(): void { this.sentNotifications = []; } } ``` **Uso:** ```typescript it('should notify reviewers when submitting', async () => { const mockNotification = new NotificationServiceMock(); const service = new MedicalValidationService( contentRepository, reviewRepository, mockNotification ); await service.submitForReview('content-id', 'editor-id'); expect(mockNotification.sentNotifications).toHaveLength(1); expect(mockNotification.sentNotifications[0].type).toBe('review_request'); }); ``` --- ## 🎯 Estrategia de Testing ### Cobertura Mínima: 80% **Distribución:** - **Domain Layer:** 90%+ (lógica crítica) - **Application Layer:** 85%+ (casos de uso) - **Infrastructure Layer:** 70%+ (implementaciones) - **Presentation Layer:** 75%+ (rutas) --- ## 📝 Tipos de Tests ### 1. Unit Tests (Tests Unitarios) **Qué testear:** - Funciones puras - Value Objects - Domain Services - Validaciones **Ejemplo:** ```typescript describe('DoseRange', () => { it('should create valid range', () => { const range = DoseRange.create(0.5, 1, 'mg', 'adulto'); expect(range.min).toBe(0.5); expect(range.max).toBe(1); }); it('should reject invalid range', () => { expect(() => { DoseRange.create(1, 0.5, 'mg', 'adulto'); }).toThrow('Dosis máxima debe ser mayor que mínima'); }); it('should validate dose correctly', () => { const range = DoseRange.create(0.5, 1, 'mg', 'adulto'); expect(range.isValid(0.75)).toBe(true); expect(range.isValid(0.25)).toBe(false); expect(range.isValid(1.5)).toBe(false); }); }); ``` --- ### 2. Integration Tests (Tests de Integración) **Qué testear:** - Servicios con repositorios mockeados - Casos de uso completos - Flujos de validación **Ejemplo:** ```typescript describe('DoseValidationService Integration', () => { let mockDrugRepo: DrugRepositoryMock; let mockDoseRangeRepo: DoseRangeRepositoryMock; let service: DoseValidationService; beforeEach(() => { mockDrugRepo = new DrugRepositoryMock(); mockDoseRangeRepo = new DoseRangeRepositoryMock(); service = new DoseValidationService(mockDrugRepo, mockDoseRangeRepo); }); it('should validate dose end-to-end', async () => { // Arrange mockDrugRepo.addDrug(mockDrugs.adrenalina); mockDoseRangeRepo.addRange('adrenalina', mockDoseRanges.adrenalinaAdulto); // Act const result = await service.validateDose( 'adrenalina', 0.5, PatientWeight.fromKg(70), PatientAge.fromYears(35), 'IV' ); // Assert expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); }); }); ``` --- ### 3. API Tests (Tests de API) **Qué testear:** - Endpoints HTTP - Validación de entrada - Respuestas correctas - Códigos de estado **Ejemplo:** ```typescript describe('POST /api/drugs/validate-dose', () => { it('should validate dose and return result', async () => { const response = await request(app) .post('/api/drugs/validate-dose') .set('Authorization', `Bearer ${token}`) .send({ drugId: 'adrenalina', dose: 0.5, weight: 70, age: 35, route: 'IV' }); expect(response.status).toBe(200); expect(response.body.valid).toBe(true); }); it('should reject invalid dose', async () => { const response = await request(app) .post('/api/drugs/validate-dose') .set('Authorization', `Bearer ${token}`) .send({ drugId: 'adrenalina', dose: 10, // Dosis letal weight: 70, age: 35, route: 'IV' }); expect(response.status).toBe(400); expect(response.body.valid).toBe(false); expect(response.body.errors).toContain(expect.stringContaining('máximo')); }); }); ``` --- ## 🏗️ Estructura de Tests ``` backend/ ├── src/ │ └── ... │ └── tests/ ├── unit/ # Tests unitarios │ ├── domain/ │ │ ├── entities/ │ │ ├── value-objects/ │ │ └── services/ │ └── application/ │ └── services/ │ ├── integration/ # Tests de integración │ ├── services/ │ └── use-cases/ │ ├── api/ # Tests de API │ └── routes/ │ ├── mocks/ # Mocks │ ├── DrugRepositoryMock.ts │ ├── ContentRepositoryMock.ts │ └── NotificationServiceMock.ts │ └── fixtures/ # Datos de prueba ├── medicalData.ts └── testUsers.ts ``` --- ## ✅ Checklist de Testing Al escribir tests: - [ ] ¿El test es independiente? (no depende de otros tests) - [ ] ¿Usa mocks para dependencias externas? - [ ] ¿Tiene Arrange-Act-Assert claro? - [ ] ¿Testea casos de éxito Y de error? - [ ] ¿Testea casos de borde? - [ ] ¿Nombres descriptivos? - [ ] ¿Un test por caso de uso? --- ## 📊 Ejemplo Completo ```typescript // tests/unit/domain/value-objects/DoseRange.test.ts import { DoseRange } from '../../../../src/domain/value-objects/DoseRange'; describe('DoseRange', () => { describe('create', () => { it('should create valid range', () => { const range = DoseRange.create(0.5, 1, 'mg', 'adulto'); expect(range.min).toBe(0.5); expect(range.max).toBe(1); expect(range.unit).toBe('mg'); }); it('should reject negative minimum', () => { expect(() => { DoseRange.create(-1, 1, 'mg', 'adulto'); }).toThrow('Dosis mínima no puede ser negativa'); }); it('should reject max < min', () => { expect(() => { DoseRange.create(1, 0.5, 'mg', 'adulto'); }).toThrow('Dosis máxima debe ser mayor que mínima'); }); }); describe('isValid', () => { it('should validate dose within range', () => { const range = DoseRange.create(0.5, 1, 'mg', 'adulto'); expect(range.isValid(0.75)).toBe(true); }); it('should reject dose below minimum', () => { const range = DoseRange.create(0.5, 1, 'mg', 'adulto'); expect(range.isValid(0.25)).toBe(false); }); it('should reject dose above maximum', () => { const range = DoseRange.create(0.5, 1, 'mg', 'adulto'); expect(range.isValid(1.5)).toBe(false); }); }); describe('getWarningLevel', () => { it('should return safe for middle range', () => { const range = DoseRange.create(0.5, 1, 'mg', 'adulto'); expect(range.getWarningLevel(0.75)).toBe('safe'); }); it('should return low for lower range', () => { const range = DoseRange.create(0.5, 1, 'mg', 'adulto'); expect(range.getWarningLevel(0.55)).toBe('low'); }); it('should return high for upper range', () => { const range = DoseRange.create(0.5, 1, 'mg', 'adulto'); expect(range.getWarningLevel(0.95)).toBe('high'); }); it('should return critical for out of range', () => { const range = DoseRange.create(0.5, 1, 'mg', 'adulto'); expect(range.getWarningLevel(2)).toBe('critical'); }); }); }); ``` --- **Fin del documento**