- ✅ 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
11 KiB
11 KiB
🧪 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?
- Aislamiento: Probar una función sin depender de otras
- Velocidad: Evitar llamadas lentas (BD, APIs)
- Control: Controlar el comportamiento de dependencias
- 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:
// 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:
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:
// 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:
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:
// 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:
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:
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:
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:
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
// 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