codigo0/docs/TESTING_MOCKS_DATOS_MEDICOS.md
planetazuzu 5d7a6500fe refactor: Fase 1 - Clean Architecture, refactorización modular y eliminación de duplicidades
-  Ticket 1.1: Estructura Clean Architecture en backend
-  Ticket 1.2: Schemas Zod compartidos
-  Ticket 1.3: Refactorización drugs.ts (1362 → 8 archivos modulares)
-  Ticket 1.4: Refactorización procedures.ts (3583 → 6 archivos modulares)
-  Ticket 1.5: Eliminación de duplicidades (~50 líneas)

Cambios principales:
- Creada estructura Clean Architecture en backend/src/
- Schemas Zod compartidos en backend/src/shared/schemas/
- Refactorización modular de drugs y procedures
- Utilidades genéricas en src/utils/ (filter, validation)
- Eliminados scripts obsoletos y documentación antigua
- Corregidos errores: QueryClient, import test-error-handling
- Build verificado y funcionando correctamente
2026-01-25 21:09:47 +01:00

484 lines
11 KiB
Markdown

# 🧪 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<string, Drug> = new Map();
async findById(id: string): Promise<Drug | null> {
return this.drugs.get(id) || null;
}
async save(drug: Drug): Promise<Drug> {
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<void> {
this.sentNotifications.push({
to: 'reviewers',
type: 'review_request',
data
});
}
async notifyEditor(data: unknown): Promise<void> {
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**