# Estrategia de pruebas (Testing Strategy) Las pruebas de software permiten verificar comportamientos, detectar regresiones y proporcionar confianza durante el desarrollo y evolución de un sistema. Su objetivo no es maximizar el número de pruebas ni alcanzar métricas de cobertura arbitrarias, sino comprobar que el software satisface correctamente sus requisitos. Este documento desarrolla las prácticas de pruebas establecidas en el `SKILL.md` principal y complementa las recomendaciones de [tdd.md](tdd.md). **Selecciona las pruebas según el comportamiento, el riesgo y el nivel de confianza necesario. No impongas una distribución fija de tipos de pruebas a todos los proyectos.** ## 1. La pirámide de pruebas (Testing Pyramid) La pirámide de pruebas es un modelo que propone combinar diferentes niveles de verificación. Tradicionalmente se representa de la siguiente forma: ```text id="gjbpx1" /\ / \ / E2E\ /------\ / \ /Integración\ /------------\ / \ / Unitarias \ /__________________\ ``` La representación sugiere mantener una base amplia de pruebas rápidas y un conjunto más reducido de pruebas que requieren integrar numerosos componentes. ### Interpretación correcta - Las pruebas unitarias suelen proporcionar retroalimentación rápida. - Las pruebas de integración verifican colaboraciones y límites técnicos. - Las pruebas E2E comprueban recorridos completos desde la perspectiva del usuario o consumidor. Sin embargo, la pirámide no establece cantidades obligatorias. La distribución adecuada depende de: - Arquitectura del sistema. - Complejidad del dominio. - Integraciones externas. - Riesgo funcional. - Criticidad de los procesos. - Facilidad para ejecutar los distintos tipos de pruebas. - Coste de mantenimiento. ### Criterios de aplicación - Utiliza pruebas unitarias cuando permitan comprobar comportamientos de forma rápida y aislada. - Incorpora pruebas de integración para los contratos y límites relevantes. - Utiliza pruebas E2E cuando sea necesario comprobar un flujo completo. - Evita duplicar indiscriminadamente los mismos escenarios en todos los niveles. - No presupongas que una prueba de mayor nivel proporciona siempre más confianza. - Selecciona el nivel más adecuado para detectar cada tipo de regresión. **Una estrategia de pruebas eficaz optimiza la confianza obtenida respecto al coste de ejecución y mantenimiento.** --- ## 2. Tipos de pruebas ### 2.1. Pruebas unitarias (Unit Tests) Las pruebas unitarias verifican unidades de comportamiento en condiciones controladas. Una unidad puede ser: - Una función. - Una clase. - Un objeto de dominio. - Un módulo pequeño. - Un conjunto cohesionado de componentes. No es obligatorio que una prueba unitaria compruebe exactamente una clase o función. ### Características habituales - Ejecución rápida. - Resultados deterministas. - Preparación sencilla. - Independencia de infraestructura externa cuando sea apropiado. - Verificación de comportamientos concretos. ### Ejemplo ```typescript id="qv1r4u" describe('Order', () => { it('calculates the total of two items', () => { const order = new Order(); order.addItem({ price: 100 }); order.addItem({ price: 50 }); expect(order.calculateTotal()).toBe(150); }); }); ``` El ejemplo utiliza importes numéricos para ilustrar la estructura. En sistemas con reglas monetarias relevantes, utiliza una representación adecuada como `Money`. ### Cuándo utilizarlas Son especialmente útiles para: - Reglas de negocio. - Cálculos. - Validaciones. - Objetos de valor. - Entidades. - Transformaciones. - Algoritmos. - Transiciones de estado. ### Criterios de aplicación - Comprueba comportamientos observables. - Evita dependencias externas innecesarias. - Utiliza objetos reales cuando sean sencillos y deterministas. - No introduzcas mocks para todas las dependencias por obligación. - Evita pruebas que dependan de métodos privados o detalles internos. - No escribas pruebas triviales que únicamente reproduzcan una implementación sin aportar confianza. **Una prueba unitaria debe proporcionar confianza sobre un comportamiento, no verificar cómo está organizado internamente el código.** --- ### 2.2. Pruebas de integración (Integration Tests) Las pruebas de integración verifican la colaboración entre componentes o sistemas. Pueden incluir: - Repositorios y bases de datos. - APIs y servicios. - Serialización y deserialización. - Adaptadores. - Sistemas de archivos. - Colas de mensajería. - Mecanismos de autenticación. - Transacciones. - Contratos entre módulos. ### Características habituales - Mayor preparación que las pruebas unitarias. - Dependencias reales o representativas. - Verificación de límites entre componentes. - Coste de ejecución variable. - Detección de errores que no aparecen en pruebas completamente aisladas. ### Ejemplo con persistencia ```typescript id="170oc9" describe('OrderRepository integration', () => { let database: TestDatabase; let repository: OrderRepository; beforeAll(async () => { database = await createTestDatabase(); repository = new PostgresOrderRepository(database.connection); }); afterAll(async () => { await database.close(); }); it('persists and retrieves an order', async () => { const order = createTestOrder(); const expectedStatus = order.status; const expectedTotal = order.calculateTotal(); await repository.save(order); const retrieved = await repository.findById(order.id); expect(retrieved).not.toBeNull(); expect(retrieved?.id.equals(order.id)).toBe(true); expect(retrieved?.status).toBe(expectedStatus); expect(retrieved?.calculateTotal()).toEqual(expectedTotal); }); }); ``` El ejemplo presupone una infraestructura de pruebas que proporciona una base de datos controlada y un pedido con `status` público y `calculateTotal()`. El total esperado debe ser un valor numérico u objeto de valor inmutable; en otros modelos, captura una proyección independiente con todos los campos y líneas que el contrato exige persistir. Para un sistema real, deben establecerse mecanismos de aislamiento y limpieza de datos entre pruebas. ### Criterios de aplicación - Verifica las integraciones que puedan producir fallos relevantes. - Utiliza dependencias reales cuando sea necesario comprobar su comportamiento. - Mantén entornos reproducibles. - Evita depender de datos de producción. - Aísla el estado de las pruebas. - Comprueba errores y comportamientos de integración relevantes. - No conviertas todas las pruebas en pruebas de integración si una prueba unitaria proporciona suficiente confianza. ### Diferencia importante Una prueba que utiliza varios objetos reales no es automáticamente una prueba de integración de infraestructura. Del mismo modo, una prueba que simula todas las dependencias externas puede estar verificando únicamente la coordinación de un componente. **Clasifica las pruebas por el comportamiento y los límites que comprueban, no únicamente por el número de objetos utilizados.** --- ### 2.3. Pruebas de extremo a extremo y aceptación (E2E / Acceptance Tests) Las pruebas E2E verifican recorridos completos desde la perspectiva del usuario o consumidor del sistema. Pueden atravesar: - Interfaz de usuario. - APIs. - Autenticación. - Aplicación. - Dominio. - Persistencia. - Integraciones. ### Características habituales - Mayor número de componentes implicados. - Preparación más compleja. - Posible mayor tiempo de ejecución. - Sensibilidad a cambios de interfaz o infraestructura. - Capacidad para detectar problemas de integración completa. ### Ejemplo ```typescript id="mq08am" test('the user can complete checkout', async ({ page }) => { await page.goto('/products'); await page.getByRole('button', { name: 'Añadir al carrito', }).click(); await page.getByRole('button', { name: 'Finalizar compra', }).click(); await page.getByRole('button', { name: 'Confirmar pedido', }).click(); await expect( page.getByRole('heading', { name: 'Pedido confirmado', }), ).toBeVisible(); }); ``` El ejemplo presupone un entorno de pruebas que permite confirmar pedidos sin ejecutar cargos reales. ### Buenas prácticas - Prioriza recorridos críticos. - Incorpora otros flujos cuando el riesgo lo justifique. - Utiliza selectores accesibles y estables. - Evita depender de textos o estructuras internas innecesarias cuando existan alternativas más robustas. - Prepara datos de prueba controlados. - Evita dependencias de servicios externos inestables. - Utiliza entornos aislados para pagos, correos y otras operaciones sensibles. - Comprueba resultados observables. No introduzcas números de tarjeta, credenciales o datos reales en las pruebas. ### Pruebas de aceptación Las pruebas de aceptación comprueban requisitos desde la perspectiva del negocio o del usuario. Pueden implementarse mediante pruebas E2E, integración u otros niveles. **Aceptación y E2E no son necesariamente conceptos equivalentes.** --- ## 3. Patrón Preparar-Actuar-Verificar (Arrange-Act-Assert) El patrón AAA organiza las pruebas en tres fases: 1. **Preparar (Arrange):** establecer los datos y condiciones necesarias. 2. **Actuar (Act):** ejecutar el comportamiento. 3. **Verificar (Assert):** comprobar el resultado. ### Ejemplo ```typescript id="4e1tbg" it('applies a 20 percent discount to premium users', () => { // Preparar. const user = new User({ isPremium: true }); const cart = new Cart(user); cart.addItem({ price: 100 }); // Actuar. const total = cart.calculateTotal(); // Verificar. expect(total).toBe(80); }); ``` ### Criterios de aplicación - Mantén identificable la operación probada. - Evita preparaciones extensas que oculten la intención. - Utiliza factorías de prueba cuando simplifiquen el escenario. - Agrupa verificaciones relacionadas. - No introduzcas abstracciones innecesarias. - No es obligatorio incluir comentarios AAA cuando la estructura resulte evidente. ### Escribir AAA en orden inverso En ocasiones resulta más sencillo diseñar la prueba comenzando por el resultado esperado: 1. **Assert:** ¿qué quiero comprobar? 2. **Act:** ¿qué operación debe producirlo? 3. **Arrange:** ¿qué datos y dependencias necesito? Este procedimiento ayuda a diseñar pruebas centradas en resultados. No es obligatorio utilizarlo para todas las pruebas. --- ## 4. Nomenclatura de las pruebas Los nombres deben describir comportamientos concretos y verificables. ### Nombres poco expresivos ```typescript id="d5nbzl" it('should work correctly', () => {}); it('handles the edge case', () => {}); it('sets the data property', () => {}); ``` ### Nombres más adecuados ```typescript id="rx85zs" it('calculates a 20 percent discount for premium users', () => {}); it('returns an error when the cart is empty', () => {}); it('recognizes racecar as a palindrome', () => {}); ``` ### Convenciones habituales **Descripción directa del comportamiento** ```typescript id="wj1e44" it('applies tax based on the shipping state', () => {}); ``` **When-Then** ```typescript id="533rfi" it('returns 5 when adding 2 and 3', () => {}); ``` **Given-When-Then** ```typescript id="idfv3p" describe('given a premium user', () => { describe('when completing checkout', () => { it('applies a 20 percent discount', () => {}); }); }); ``` Las cadenas de texto utilizadas como nombres de prueba pueden redactarse en castellano cuando el proyecto lo establezca. ### Recomendaciones - Utiliza lenguaje del dominio. - Describe resultados observables. - Evita referencias innecesarias a propiedades privadas. - Mantén una convención coherente. - No anides bloques `describe` excesivamente. - Evita nombres que dependan de una implementación concreta. ### Una responsabilidad por prueba Como orientación, cada prueba debe comprobar un comportamiento concreto. Esto no implica limitarla a una única aserción. ```typescript id="jkx484" it('creates an order with its initial state', () => { const order = createTestOrder(); expect(order.status).toBe('pending'); expect(order.items).toHaveLength(0); expect(order.total).toBe(0); }); ``` Las tres comprobaciones corresponden al mismo comportamiento inicial. --- ## 5. Dobles de prueba (Test Doubles) Los dobles de prueba permiten sustituir colaboradores durante la verificación de un componente. Su utilización debe responder a una necesidad concreta. ### 5.1. Dummy Objeto que se proporciona porque una interfaz o constructor lo exige, pero que no interviene en el comportamiento probado. ```typescript id="k3ajk6" const dummyLogger: Logger = { log(): void {}, }; const service = new UserService(repository, dummyLogger); ``` Evita utilizar aserciones de tipo inseguras como `{} as Logger` cuando puedas proporcionar una implementación mínima válida. ### 5.2. Stub Proporciona respuestas predefinidas. ```typescript id="9qsma1" const stubRepository: UserRepository = { async findById(_id: UserId): Promise { return createTestUser(); }, async save(_user: User): Promise {}, }; ``` Los stubs permiten controlar escenarios sin depender de infraestructura externa. ### 5.3. Spy Registra las llamadas realizadas a una operación. ```typescript id="h0x6i6" const sentEmails: string[] = []; const emailSpy: EmailService = { async send(to: string, _message: string): Promise { sentEmails.push(to); }, }; ``` Verificación: ```typescript id="1xz90c" expect(sentEmails).toContain('user@example.com'); ``` ### 5.4. Mock Permite definir o verificar interacciones esperadas. Ejemplo con Vitest: ```typescript id="1bcpjq" const sendMock = vi.fn(async (_to: string, _message: string) => {}); const emailService: EmailService = { send: sendMock, }; ``` Después de ejecutar el comportamiento: ```typescript id="5qktnx" expect(sendMock).toHaveBeenCalledWith( 'user@example.com', 'Pedido confirmado', ); ``` El ejemplo utiliza Vitest. En proyectos con Jest deben emplearse sus APIs equivalentes. No mezcles mecanismos de Jest y Vitest indiscriminadamente. ### 5.5. Fake Es una implementación funcional simplificada de una dependencia. ```typescript id="88n8bh" class InMemoryUserRepository implements UserRepository { private readonly users = new Map(); async save(user: User): Promise { this.users.set(user.id.value, user); } async findById(id: UserId): Promise { return this.users.get(id.value) ?? null; } } ``` Esta implementación puede permitir pruebas rápidas sin utilizar una base de datos real. ### Criterios para seleccionar dobles - Utiliza objetos reales cuando sean sencillos y deterministas. - Utiliza stubs para controlar respuestas. - Utiliza spies o mocks cuando las interacciones formen parte del comportamiento relevante. - Utiliza fakes cuando una implementación simplificada facilite las pruebas. - Evita simular detalles internos que no pertenecen al contrato. - No introduzcas mocks únicamente para alcanzar un aislamiento artificial. **Una prueba con demasiadas simulaciones puede verificar únicamente las expectativas del propio test, sin proporcionar confianza suficiente sobre el sistema real.** --- ## 6. Estrategia de pruebas por responsabilidad arquitectónica La estrategia debe adaptarse al diseño del proyecto. ### 6.1. Dominio Comprueba: - Reglas de negocio. - Objetos de valor. - Entidades. - Invariantes. - Cálculos. - Transiciones de estado. Las pruebas unitarias suelen resultar apropiadas cuando el dominio puede ejecutarse sin infraestructura externa. ```typescript id="jxbk58" describe('Money', () => { it('adds amounts with the same currency', () => { const first = Money.euros(10); const second = Money.euros(20); const result = first.add(second); expect(result.equals(Money.euros(30))).toBe(true); }); it('rejects amounts with different currencies', () => { const euros = Money.euros(10); const dollars = Money.dollars(10); expect(() => euros.add(dollars)).toThrow(CurrencyMismatchError); }); }); ``` ### 6.2. Aplicación Comprueba: - Coordinación de casos de uso. - Interacciones entre colaboradores. - Políticas de aplicación. - Tratamiento de errores. - Resultados observables. ```typescript id="7bzp6w" describe('CreateOrderUseCase', () => { it('persists a valid order', async () => { const repository = new InMemoryOrderRepository(); const useCase = new CreateOrderUseCase(repository); const result = await useCase.execute( createValidOrderRequest(), ); expect(result.success).toBe(true); expect(await repository.count()).toBe(1); }); }); ``` No todas las pruebas de aplicación necesitan simular infraestructura. ### 6.3. Infraestructura Comprueba: - Persistencia. - Consultas. - Adaptadores. - Serialización. - Integraciones. - Transacciones. - Comportamientos específicos de tecnología. ```typescript id="gu3tuw" describe('PostgresOrderRepository', () => { it('persists and retrieves an order', async () => { const repository = createTestPostgresRepository(); const order = createTestOrder(); const expectedStatus = order.status; const expectedTotal = order.calculateTotal(); await repository.save(order); const retrieved = await repository.findById(order.id); expect(retrieved?.id.equals(order.id)).toBe(true); expect(retrieved?.status).toBe(expectedStatus); expect(retrieved?.calculateTotal()).toEqual(expectedTotal); }); }); ``` Para pruebas reales, el repositorio debe utilizar una conexión controlada y un mecanismo adecuado de aislamiento de datos. Este ejemplo presupone el mismo contrato ilustrativo de pedido (`status` y `calculateTotal()`) descrito en la sección 2.2; amplía la comparación a todos los campos y líneas persistidos del modelo real. ### 6.4. Frontend Comprueba según corresponda: - Renderizado y comportamiento de componentes. - Interacción del usuario. - Formularios. - Validaciones. - Accesibilidad. - Gestión de estados. - Integración con APIs. - Flujos críticos. Prioriza pruebas que interactúen mediante comportamientos observables y no mediante detalles internos del framework. ### 6.5. Código compartido Comprueba: - Contratos. - Esquemas. - Serialización. - Validaciones. - Compatibilidad. - Transformaciones. - Invariantes comunes. La reutilización de un tipo entre frontend y backend no garantiza que los datos recibidos cumplan dicho tipo durante la ejecución. --- ## 7. Pruebas de integración de alto valor Prioriza las pruebas de integración en los límites donde los errores puedan tener consecuencias importantes. ### 7.1. Límites entre sistemas Ejemplos: - API y base de datos. - Aplicación y cola. - Servicio y proveedor externo. - Serialización y validación. - Autenticación y autorización. - Persistencia y transacciones. ### 7.2. Procesos críticos Presta especial atención a: - Pagos. - Facturación. - Autorización. - Integridad de datos. - Operaciones irreversibles. - Procesos empresariales esenciales. ### 7.3. Consultas complejas Comprueba consultas que incluyan: - Relaciones complejas. - Agregaciones. - Filtros relevantes. - Restricciones de integridad. - Paginación. - Concurrencia. - Comportamientos específicos del motor. No presupongas que una consulta funciona correctamente únicamente porque su lógica parece adecuada al inspeccionar el código. --- ## 8. Pruebas de contrato (Contract Tests) Las pruebas de contrato permiten comprobar que diferentes implementaciones respetan un comportamiento común. Son especialmente útiles para: - Repositorios. - Adaptadores. - Proveedores externos. - Implementaciones intercambiables. - APIs entre aplicaciones. - Contratos compartidos entre frontend y backend. ### Ejemplo de contrato compartido El ejemplo presupone que `User` expone `id: UserId`, `name: string` y `email: string`, y que `createTestUser(overrides)` crea identificadores únicos y respeta los datos recibidos. Son contratos ilustrativos: adapta las proyecciones a las propiedades públicas o valores del modelo real. Captura los valores esperados antes de guardar; comprobar solo la identidad no detecta la pérdida de otros campos. ```typescript id="5h26e3" function testUserRepositoryContract( createRepository: () => Promise, resetRepository: () => Promise, ): void { describe('UserRepository contract', () => { let repository: UserRepository; beforeEach(async () => { await resetRepository(); repository = await createRepository(); }); it('persists and retrieves a user', async () => { const expectedName = 'Contract User 47'; const expectedEmail = 'contract-47@example.com'; const user = createTestUser({ name: expectedName, email: expectedEmail, }); await repository.save(user); const retrieved = await repository.findById(user.id); expect(retrieved).not.toBeNull(); expect(retrieved?.id.equals(user.id)).toBe(true); expect(retrieved?.name).toBe(expectedName); expect(retrieved?.email).toBe(expectedEmail); }); it('keeps the persisted data of different users independent', async () => { const first = createTestUser({ name: 'First Contract User', email: 'first-contract@example.com', }); const second = createTestUser({ name: 'Second Contract User', email: 'second-contract@example.com', }); await repository.save(first); await repository.save(second); const retrievedFirst = await repository.findById(first.id); const retrievedSecond = await repository.findById(second.id); expect(retrievedFirst?.id.equals(first.id)).toBe(true); expect(retrievedFirst?.name).toBe('First Contract User'); expect(retrievedFirst?.email).toBe('first-contract@example.com'); expect(retrievedSecond?.id.equals(second.id)).toBe(true); expect(retrievedSecond?.name).toBe('Second Contract User'); expect(retrievedSecond?.email).toBe('second-contract@example.com'); }); it('returns null when the user does not exist', async () => { const missingId = createMissingUserId(); const retrieved = await repository.findById(missingId); expect(retrieved).toBeNull(); }); }); } ``` Aplicación a distintas implementaciones: ```typescript id="2ov5k9" testUserRepositoryContract( async () => new InMemoryUserRepository(), async () => resetInMemoryRepository(), ); ``` ```typescript id="hhpi1w" testUserRepositoryContract( async () => createPostgresUserRepository(), async () => resetTestDatabase(), ); ``` ### Criterios importantes - Verifica comportamientos, no únicamente firmas. - Comprueba resultados y errores esperados. - Mantén las pruebas independientes. - Utiliza una estrategia de limpieza adecuada. - Comprueba las garantías compartidas por todas las implementaciones. - Si `save` permite actualizar usuarios, añade una prueba que cambie un campo, guarde y lo recupere de nuevo. No presupongas una semántica de actualización que el contrato no haya definido. - No presupongas que una implementación en memoria reproduce exactamente el comportamiento de una base de datos real. ### Contratos entre servicios Cuando existan APIs entre frontend y backend o entre servicios, considera: - Validación de esquemas. - Compatibilidad de versiones. - Campos obligatorios y opcionales. - Errores contractuales. - Formatos de fecha y números. - Identificadores. - Cambios compatibles e incompatibles. Las pruebas de contrato deben complementar, no sustituir automáticamente, las pruebas de integración relevantes. --- ## 9. Constructores de datos de prueba (Test Builders) Los *Test Builders* facilitan preparar objetos complejos para escenarios de prueba. ### Cuándo utilizarlos - Los objetos requieren numerosos datos. - Existen valores predeterminados útiles. - Los escenarios necesitan variaciones frecuentes. - La preparación se repite en numerosas pruebas. ### Ejemplo ```typescript id="2cdvvu" interface OrderProps { id: string; customerId: string; items: Item[]; status: 'pending' | 'paid'; } ``` ```typescript id="qj5fhj" class OrderBuilder { private id = 'order-1'; private customerId = 'customer-1'; private items: Item[] = []; private status: OrderProps['status'] = 'pending'; withId(id: string): this { this.id = id; return this; } withItems(items: Item[]): this { this.items = [...items]; return this; } paid(): this { this.status = 'paid'; return this; } build(): Order { return Order.create({ id: this.id, customerId: this.customerId, items: [...this.items], status: this.status, }); } } ``` Uso: ```typescript id="xyal0i" const order = new OrderBuilder() .withItems([{ sku: 'ABC', price: 100 }]) .paid() .build(); ``` ### Buenas prácticas - Proporciona datos predeterminados válidos. - Permite modificar únicamente los campos relevantes del escenario. - Evita compartir referencias mutables entre pruebas. - No utilices aserciones de tipo para ocultar datos obligatorios ausentes. - Mantén los builders sencillos. - No reproduzcas toda la lógica de producción dentro del builder. ### Alternativa mediante factorías Para objetos sencillos, una función suele ser suficiente: ```typescript id="mxnt4u" function createTestUser( overrides: Partial = {}, ): User { return User.create({ name: 'Test User', email: 'test@example.com', ...overrides, }); } ``` No introduzcas una clase Builder cuando una factoría de prueba clara resuelva adecuadamente el problema. --- ## 10. Aislamiento y fiabilidad Una prueba debe producir resultados reproducibles bajo las mismas condiciones. ### 10.1. Evitar estado compartido No permitas que una prueba dependa del orden de ejecución de otras. - Reinicia datos entre pruebas. - Evita variables globales mutables compartidas. - Limpia los recursos creados. - Utiliza instancias independientes cuando corresponda. ### 10.2. Controlar tiempo y aleatoriedad Cuando el comportamiento dependa de: - Fecha actual. - Zona horaria. - Temporizadores. - Valores aleatorios. - Identificadores generados. Utiliza mecanismos controlados para reproducir el escenario. No dependas del reloj real cuando pueda producir resultados inestables. ### 10.3. Controlar concurrencia Las pruebas paralelas pueden interferir si comparten: - Base de datos. - Archivos. - Puertos. - Variables globales. - Recursos externos. Define mecanismos de aislamiento adecuados. ### 10.4. Gestionar recursos Libera correctamente: - Conexiones. - Servidores. - Archivos temporales. - Suscripciones. - Temporizadores. - Procesos externos. Utiliza los mecanismos de preparación y limpieza proporcionados por el framework de pruebas. ### 10.5. Evitar pruebas inestables (Flaky Tests) Una prueba inestable puede pasar o fallar sin cambios relevantes en el código. Causas habituales: - Condiciones de carrera. - Esperas temporales arbitrarias. - Dependencias externas. - Datos compartidos. - Orden de ejecución. - Operaciones asíncronas no esperadas. No soluciones la inestabilidad aumentando indiscriminadamente los tiempos de espera. Identifica y corrige la causa. **Una prueba inestable reduce la confianza en toda la batería de pruebas.** --- ## 11. Pruebas asíncronas En aplicaciones TypeScript y Node.js, las pruebas deben esperar correctamente las operaciones asíncronas. ### Ejemplo incorrecto ```typescript id="f31do9" it('saves the order', () => { orderRepository.save(order); expect(orderRepository.count()).toBe(1); }); ``` La operación puede continuar ejecutándose después de la aserción. ### Alternativa ```typescript id="3hz4v4" it('saves the order', async () => { await orderRepository.save(order); const count = await orderRepository.count(); expect(count).toBe(1); }); ``` ### Recomendaciones - Espera las promesas relevantes. - No ignores rechazos. - Evita temporizadores arbitrarios. - Gestiona correctamente la limpieza asíncrona. - Utiliza mecanismos de espera basados en condiciones cuando corresponda. - Comprueba errores mediante aserciones apropiadas. Ejemplo: ```typescript id="8wzl6r" await expect( orderRepository.save(invalidOrder), ).rejects.toThrow(InvalidOrderError); ``` --- ## 12. Pruebas de regresión Cuando se corrige un defecto reproducible, conviene añadir una prueba que permita detectar su reaparición. ### Procedimiento recomendado 1. Reproduce el problema. 2. Identifica el comportamiento esperado. 3. Escribe una prueba que falle por el defecto, cuando sea viable. 4. Corrige la implementación. 5. Comprueba que la prueba pasa. 6. Ejecuta las verificaciones relacionadas. ### Código legado Cuando no exista infraestructura adecuada: - Analiza el comportamiento actual. - Utiliza pruebas de caracterización cuando resulte posible. - Mantén los cambios acotados. - Comprueba los contratos afectados. - Añade pruebas de regresión cuando sea viable. No utilices la ausencia de pruebas como una razón automática para impedir correcciones necesarias. --- ## 13. Cobertura de pruebas La cobertura indica qué partes del código han sido ejecutadas durante las pruebas. Puede ayudar a detectar áreas poco verificadas, pero no demuestra que los comportamientos sean correctos. ### Consideraciones - Una cobertura elevada no garantiza pruebas de calidad. - Una cobertura baja puede indicar riesgos relevantes. - No escribas pruebas sin significado únicamente para aumentar porcentajes. - Prioriza reglas de negocio, casos límite y procesos críticos. - Evalúa la cobertura junto con la calidad de las aserciones y los escenarios. ### Métricas habituales - Cobertura de líneas. - Cobertura de funciones. - Cobertura de ramas. - Cobertura de instrucciones. Las métricas pueden utilizarse como indicadores de revisión. No establezcas umbrales universales sin considerar el tipo de sistema y los criterios específicos del proyecto. --- ## 14. Errores habituales en las pruebas | Error | Problema | Recomendación | |---|---|---| | Probar detalles internos | Pruebas frágiles ante refactorizaciones correctas. | Verificar comportamientos observables. | | Utilizar demasiados mocks | Las pruebas reproducen expectativas artificiales. | Utilizar objetos reales cuando corresponda. | | Compartir estado | Resultados dependientes del orden de ejecución. | Aislar escenarios. | | No incluir aserciones relevantes | Falsa confianza. | Verificar resultados significativos. | | Probar únicamente código trivial | Bajo valor respecto al esfuerzo. | Priorizar lógica y riesgos. | | Pruebas excesivamente lentas | Retroalimentación tardía. | Optimizar aislamiento y ejecución. | | Dependencias externas inestables | Fallos no deterministas. | Utilizar entornos controlados. | | Datos de producción | Riesgos de privacidad e integridad. | Utilizar datos sintéticos o preparados. | | Modificar expectativas para hacer pasar pruebas | Ocultación de defectos. | Investigar el comportamiento correcto. | | Eliminar pruebas que fallan | Pérdida de protección frente a regresiones. | Analizar y corregir la causa. | | No esperar operaciones asíncronas | Resultados inconsistentes. | Utilizar correctamente `async/await`. | | Aplicar una pirámide rígida | Pruebas inadecuadas para la arquitectura. | Ajustar la estrategia a los riesgos reales. | --- ## 15. Disciplina de pruebas para agentes de IA Al modificar código o pruebas: - No cambies expectativas únicamente para conseguir que las pruebas pasen. - No elimines pruebas que fallen sin analizar su causa. - No desactives validaciones ni controles de seguridad para simplificar un escenario. - No introduzcas mocks que oculten comportamientos incorrectos. - No cambies herramientas de pruebas sin una necesidad justificada. - Respeta las convenciones y frameworks existentes. - Limita los cambios a los archivos relacionados con la tarea. - Añade pruebas de regresión cuando corrijas defectos reproducibles y resulte viable. - No inventes resultados de ejecución. - No declares que una prueba ha pasado si no se ha ejecutado. ### Cuando una prueba existente falla 1. Comprueba si el fallo ya existía. 2. Analiza el comportamiento esperado. 3. Identifica si existe un defecto funcional o una expectativa obsoleta. 4. Corrige el código o la prueba según corresponda. 5. Justifica cualquier cambio contractual. 6. Ejecuta nuevamente las verificaciones pertinentes. ### Cuando no pueden ejecutarse pruebas Indica: - Qué verificaciones no se ejecutaron. - Por qué no pudieron ejecutarse. - Qué otras comprobaciones se realizaron. - Qué riesgos o incertidumbres permanecen. **La transparencia sobre las verificaciones realizadas forma parte de la calidad de la entrega.** --- ## 16. Lista de comprobación de pruebas ### Diseño - [ ] ¿Qué comportamiento necesito verificar? - [ ] ¿Qué nivel de prueba resulta más apropiado? - [ ] ¿La prueba aporta confianza real? - [ ] ¿Estoy evitando comprobar detalles internos innecesarios? - [ ] ¿El escenario representa un requisito o riesgo relevante? ### Preparación - [ ] ¿Los datos de prueba son válidos y controlados? - [ ] ¿Las dependencias están correctamente aisladas? - [ ] ¿He evitado estado compartido? - [ ] ¿Las operaciones asíncronas se esperan correctamente? - [ ] ¿Los recursos se limpian al finalizar? ### Verificación - [ ] ¿Las aserciones comprueban el comportamiento esperado? - [ ] ¿Los casos límite relevantes están cubiertos? - [ ] ¿Los errores esperados se verifican correctamente? - [ ] ¿La prueba es determinista? - [ ] ¿He evitado simulaciones innecesarias? ### Integración - [ ] ¿Los contratos relevantes están comprobados? - [ ] ¿Las integraciones críticas disponen de pruebas adecuadas? - [ ] ¿Los datos y recursos externos están aislados? - [ ] ¿Las implementaciones alternativas respetan los contratos? - [ ] ¿Se han considerado transacciones y concurrencia cuando corresponde? ### Finalización - [ ] ¿Se han ejecutado las pruebas relacionadas con los cambios? - [ ] ¿Se han ejecutado las comprobaciones adicionales necesarias? - [ ] ¿Las pruebas existentes siguen siendo válidas? - [ ] ¿Se han incorporado pruebas de regresión cuando corresponde? - [ ] ¿He comunicado las verificaciones no realizadas? - [ ] ¿Puedo afirmar con precisión qué comportamiento se ha verificado? --- ## Principio final > "Una buena prueba proporciona confianza sobre un comportamiento relevante y permite modificar el software sin introducir regresiones inesperadas." Utiliza pruebas unitarias, de integración, de contrato y E2E según las necesidades del sistema. Prioriza escenarios significativos, resultados observables y mecanismos reproducibles. No conviertas la cobertura, el número de pruebas o la estructura de la pirámide en objetivos independientes de la calidad del software. **La mejor estrategia de pruebas es la que proporciona confianza suficiente en los comportamientos importantes con un coste razonable de ejecución y mantenimiento.**