Testing Guide
Testing Guide
Sección titulada «Testing Guide»Referencia operativa para escribir y correr tests. Las decisiones de arquitectura están en [[adr-009-testing-strategy|ADR-009]] y sus sub-documentos.
Comandos de ejecución
Sección titulada «Comandos de ejecución»Frontend (desde frontend/)
Sección titulada «Frontend (desde frontend/)»bun test # todos los tests, modo watchbunx vitest run # todos los tests, modo CIbunx vitest run path/to/file.spec.ts # un spec específicobunx vitest run --coverage # con reporte de cobertura v8bun run test:static # guards de arquitectura estáticos (vitest.static.config.ts)Backend (desde backend/ o vía Docker)
Sección titulada «Backend (desde backend/ o vía Docker)»pytest # todos los testspytest -m api # suite de contratos únicamentepytest -m "not slow" # omitir tests lentospytest -m regression # suite de regresiónpytest apps/tags/tests/ # tests de una apppytest tests/contract/test_employees_contract.py::TestEmployeeList # una claseEstructura actual
Sección titulada «Estructura actual»Backend
Sección titulada «Backend»backend/├── conftest.py # fixtures raíz (db, client, auth)├── tests/│ ├── test_health.py│ ├── test_api_contract.py│ ├── test_regression_backlog.py│ ├── test_contract_coverage.py # verifica que todos los endpoints de API.md tengan test│ └── contract/│ ├── test_health_contract.py│ ├── test_auth_contract.py│ ├── test_employees_contract.py│ ├── test_org_units_contract.py│ ├── test_tags_contract.py│ ├── test_positions_contract.py│ ├── test_assignments_contract.py│ ├── test_audit_log_contract.py│ ├── test_hours_ledger_contract.py│ ├── test_coverage_contract.py│ └── test_business_rules_contract.py├── apps/│ ├── fsm/tests/│ │ ├── test_employee_fsm.py│ │ ├── test_audit_log.py│ │ └── test_expire_proposals.py│ ├── tags/tests/│ │ ├── conftest.py│ │ ├── test_models.py│ │ ├── test_endpoints.py│ │ ├── test_hours_ledger.py│ │ └── test_tag_requirement_rule.py│ ├── demand/tests/│ │ ├── test_org_unit.py│ │ ├── test_coverage_summary.py│ │ ├── test_position_eligibility.py│ │ ├── test_position_duplicate.py│ │ └── test_position_delete.py│ ├── audit_log/tests/│ │ ├── test_endpoint.py│ │ └── test_tag_change_log.py│ └── business_rules/tests/│ └── test_engine.pyFrontend
Sección titulada «Frontend»49 archivos .spec.ts en frontend/src/. Estructura representativa:
src/app/├── app.routes.spec.ts # guard de rutas├── auth/│ ├── login.component.spec.ts│ └── login-card.component.spec.ts├── core/│ ├── guards/auth.guard.spec.ts│ ├── interceptors/auth.interceptor.spec.ts│ ├── interceptors/error.interceptor.spec.ts│ └── services/auth.service.spec.ts├── features/│ ├── no-mock-imports.spec.ts # guard arquitectónico: prohíbe mocks en features│ ├── roster/...│ ├── structure/...│ └── org-chart/...└── shared/ ├── mocks/clinic-bienestar.spec.ts # valida integridad del JSON de mock data └── components/... # un spec por componente sharedFrontend: patrones de testing de signals
Sección titulada «Frontend: patrones de testing de signals»signal() y computed()
Sección titulada «signal() y computed()»Replicar la función pura y testear con inputs explícitos — sin TestBed.
// El componente tiene: protected occupancyColor = computed(() => { ... });// El spec replica la función:function occupancyColor(h: HoursData | null): string { ... }
it('returns red below 50%', () => { expect(occupancyColor({ occupancy: 49, ... })).toBe('var(--p-red-500)');});linkedSignal()
Sección titulada «linkedSignal()»Igual que computed(). Si depende de un padre: TestBed + TestBed.flushEffects().
effect()
Sección titulada «effect()»TestBed.runInInjectionContext(() => { const s = signal(0); effect(() => { sideEffectFn(s()); });});TestBed.flushEffects();expect(sideEffectFn).toHaveBeenCalledWith(0);httpResource()
Sección titulada «httpResource()»TestBed.configureTestingModule({ providers: [provideHttpClient(), provideHttpClientTesting(), MyService],});const svc = TestBed.inject(MyService);const httpMock = TestBed.inject(HttpTestingController);
httpMock.expectOne('/api/v1/employees/').flush({ results: [...] });await flushMicrotasks();expect(svc.employees()).toEqual([...]);httpMock.verify();Frontend: design system guards
Sección titulada «Frontend: design system guards»- Definir el regex prohibido en el spec del componente (o en un helper de auditoría si es global).
- Afirmar
forbiddenPattern.test(templateSource)esfalse. - Si es global, agregar al helper y verificar que corra en cada spec de componente.
Frontend: zona horaria (ADR-015)
Sección titulada «Frontend: zona horaria (ADR-015)»Tests que evalúan lógica de horas dependen de la zona horaria de la máquina. Opciones:
- Fijar
TZ=UTCen CI (preferido). - Mockear
Dateconvi.setSystemTime()y offset UTC explícito. - Documentar la dependencia en el encabezado del spec.
No usar getUTCHours() en lógica de aplicación — solo en el harness de tests.
Backend: contratos
Sección titulada «Backend: contratos»Cada archivo en tests/contract/ verifica: happy path (status + campos), 401 sin auth, un caso de error, campos de paginación si aplica.
El verificador tests/test_contract_coverage.py parsea API.md, extrae todos los endpoints ### METHOD /api/... y falla si alguno no tiene test. Mantiene un conjunto KNOWN_UNTESTED para deferrals intencionales.
Backend: conftest topology (estado actual)
Sección titulada «Backend: conftest topology (estado actual)»| Ámbito | Estado |
|---|---|
backend/ (raíz) | conftest.py presente — fixtures globales |
apps/tags/tests/ | conftest.py presente |
apps/users/tests/ | sin conftest — agregar cuando se expandan tests |
apps/demand/tests/ | sin conftest — agregar cuando se expandan tests |
apps/assignments/tests/ | sin conftest — agregar cuando se escriban tests |
apps/fsm/tests/ | sin conftest |
apps/audit_log/tests/ | sin conftest |
apps/business_rules/tests/ | sin conftest |
Backend: escenarios de dominio pendientes
Sección titulada «Backend: escenarios de dominio pendientes»Identificados en el gap analysis 2026-04. Cubrir cuando se implementen los módulos.
Hours Ledger (apps/hours/tests/test_services.py)
Sección titulada «Hours Ledger (apps/hours/tests/test_services.py)»| Escenario | Comportamiento |
|---|---|
| Empleado sin contrato activo | compute_balance() lanza error o retorna cero |
| Contrato que inicia en mitad de período | Prorrateado desde inicio de contrato, no del período |
| Solapamiento de assignments concurrentes | Rechazado a nivel de modelo |
| Assignment de cero horas | Rechazado (constraint de modelo) |
| Delta de horas por tag | Ajusta el balance computado correctamente |
Coverage Rollup (apps/coverage/tests/test_aggregation.py)
Sección titulada «Coverage Rollup (apps/coverage/tests/test_aggregation.py)»| Escenario | Comportamiento |
|---|---|
| Todos los puestos cubiertos | 100% |
| Sin assignments | 0% |
| Servicio agrega desde unidades hijas | Promedio ponderado |
| Departamento agrega desde servicios hijos | Correcto en dos niveles |
| Clínica agrega desde todos los descendientes | Rollup de árbol completo |
| OrgUnit sin puestos | null o excluida del promedio padre |
Frontend: gaps conocidos
Sección titulada «Frontend: gaps conocidos»| Faltante | Prioridad |
|---|---|
Tests de employee.service | Alta |
Tests de assignment.service | Alta |
Tests de hours-ledger.service | Media |
Tests de shell.component | Baja |
Guard no-subscribe.spec.ts | Alta |
| Automatización de accesibilidad (axe-core) | Diferido |
| Tests E2E | Diferido |