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

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?

  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:

// 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