- ✅ 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
484 lines
11 KiB
Markdown
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**
|