# Desarrollo guiado por pruebas (Test-Driven Development) El desarrollo guiado por pruebas (TDD) es una metodología que utiliza pruebas automatizadas para orientar la implementación y evolución del software. El objetivo no es simplemente aumentar el número de pruebas, sino diseñar código verificable, expresar claramente los comportamientos esperados y reducir el riesgo de introducir regresiones. Este documento desarrolla los principios definidos en el `SKILL.md` principal. **TDD es la metodología preferente cuando resulta apropiada, pero no debe imponerse de forma mecánica si existe una alternativa de verificación más adecuada.** ## 1. El ciclo fundamental de TDD ```text ROJO → VERDE → REFACTORIZAR → ROJO → ... ``` Cada iteración debe ser pequeña y centrarse en un comportamiento concreto. ### 1.1. Fase ROJO (RED) Escribe una prueba que falle y describa el comportamiento que deseas implementar. La prueba debe: - Utilizar lenguaje del dominio, evitando detalles técnicos innecesarios. - Describir **QUÉ** debe ocurrir, no **CÓMO** debe implementarse. - Representar un ejemplo concreto y verificable. - Fallar por la ausencia del comportamiento esperado, no por errores accidentales de configuración. - Expresar claramente el resultado esperado. ```typescript // MAL: Descripción demasiado abstracta. it('can add numbers', () => { expect(add(2, 3)).toBe(5); }); // BIEN: Ejemplo concreto. it('returns 5 when adding 2 and 3', () => { expect(add(2, 3)).toBe(5); }); ``` Aunque las descripciones de pruebas son cadenas de texto, sigue las convenciones del proyecto. Si se consideran documentación del comportamiento, pueden redactarse en castellano. **Comprueba que la prueba falla por el motivo esperado antes de continuar.** Una prueba que falla por un error de importación, una dependencia ausente o una configuración incorrecta no demuestra necesariamente que el comportamiento esté pendiente de implementar. ### 1.2. Fase VERDE (GREEN) Escribe la implementación más sencilla que permita superar la prueba. Evita introducir funcionalidades, configuraciones o abstracciones que todavía no sean necesarias. Existen dos estrategias habituales. **Estrategia 1: Fake It (simular inicialmente el resultado)** Consiste en utilizar una implementación provisional mínima para superar un ejemplo concreto. ```typescript function add(a: number, b: number): number { return 5; } ``` Esta implementación únicamente satisface un ejemplo específico. Puede utilizarse como paso temporal durante el aprendizaje de TDD o cuando se busca descubrir una generalización mediante pruebas adicionales. **No debe conservarse como implementación final si el comportamiento requerido exige una solución general.** **Estrategia 2: Obvious Implementation (implementación evidente)** Cuando la solución es sencilla y conocida, implementa directamente el comportamiento. ```typescript function add(a: number, b: number): number { return a + b; } ``` No es necesario utilizar *Fake It* si la implementación correcta resulta evidente. ### Criterios para elegir la estrategia - Utiliza *Fake It* cuando necesites avanzar incrementalmente hacia una generalización. - Utiliza *Obvious Implementation* cuando el comportamiento sea sencillo y conocido. - Evita construir una arquitectura completa a partir de un único ejemplo. - No introduzcas casos hipotéticos sin una necesidad identificada. - Asegúrate de que el conjunto de pruebas comprueba el comportamiento realmente requerido. **La simplicidad debe evaluarse respecto al comportamiento esperado, no únicamente respecto a la cantidad de código.** ### 1.3. Fase REFACTORIZAR (REFACTOR) Una vez que las pruebas pasan, mejora el diseño sin modificar el comportamiento observable. Revisa: - Duplicación de conocimiento o reglas de negocio. - Métodos o clases con responsabilidades mezcladas. - Nombres poco expresivos. - Condiciones difíciles de comprender. - Dependencias innecesarias. - Abstracciones prematuras. - Estructuras que dificulten el mantenimiento. La refactorización debe realizarse en incrementos pequeños, verificando que las pruebas continúan pasando. **No refactorices únicamente para satisfacer métricas arbitrarias de líneas, atributos o número de métodos.** El diseño puede evolucionar durante la refactorización, pero también requiere decisiones previas cuando sea necesario definir contratos, límites entre módulos o responsabilidades arquitectónicas. --- ## 2. Las tres leyes del TDD Durante la aplicación estricta del ciclo TDD: 1. **No escribas código de producción** salvo para conseguir que una prueba que falla pase correctamente. 2. **No escribas más código de prueba** del necesario para demostrar el fallo del comportamiento esperado. 3. **No escribas más código de producción** del necesario para superar la prueba que falla. Estas leyes favorecen los incrementos pequeños y evitan implementar funcionalidades no solicitadas. No deben interpretarse como una prohibición universal de escribir código cuando TDD no sea la metodología apropiada. ### Excepciones justificadas Puede utilizarse otro procedimiento cuando: - Se modifique configuración, documentación o infraestructura sin comportamiento directamente comprobable mediante pruebas unitarias. - Se trabaje con código legado sin infraestructura de pruebas adecuada. - Se realicen prototipos exploratorios. - Sea necesario corregir urgentemente un problema y no resulte viable escribir primero una prueba. - Se efectúen modificaciones estructurales cubiertas por pruebas existentes. En estos casos: 1. Identifica el comportamiento que debe mantenerse o modificarse. 2. Selecciona el mecanismo de verificación más adecuado. 3. Realiza cambios pequeños y controlados. 4. Ejecuta las comprobaciones disponibles. 5. Añade pruebas de regresión cuando resulte técnicamente viable. **No utilizar TDD no significa trabajar sin verificación.** --- ## 3. La regla de tres La regla de tres es una heurística para identificar oportunidades de abstracción. Tradicionalmente propone esperar hasta observar varias repeticiones antes de extraer código común. Su propósito es evitar crear abstracciones incorrectas a partir de similitudes superficiales. ```typescript // Primera aparición: implementar directamente. // Segunda aparición: comprobar si existe conocimiento compartido. // Tercera aparición: reconsiderar si conviene extraer una abstracción. ``` ### Aplicación recomendada - No extraigas automáticamente código después de la tercera repetición. - No esperes obligatoriamente a tres repeticiones cuando ya exista una regla de negocio compartida. - Comprueba que las implementaciones representen el mismo concepto. - Evita abstraer código que únicamente se parece sintácticamente. - Valora el coste de mantenimiento de la duplicación frente al coste de la abstracción. **La duplicación de conocimiento puede ser más peligrosa que la duplicación de instrucciones.** Por ejemplo, dos implementaciones independientes de una misma regla fiscal pueden justificar una extracción inmediata. En cambio, tres operaciones similares pertenecientes a dominios diferentes pueden mantenerse separadas. > "Un poco de duplicación es mejor que una abstracción equivocada." --- ## 4. Triangulación La triangulación consiste en utilizar varios ejemplos concretos para orientar la implementación hacia una solución general. Cada prueba adicional restringe las implementaciones posibles y ayuda a descubrir reglas que satisfacen todos los casos. ### Ejemplo Primera prueba: ```typescript it('returns 5 when adding 2 and 3', () => { expect(add(2, 3)).toBe(5); }); ``` Una implementación provisional podría devolver siempre `5`. Segunda prueba: ```typescript it('returns 9 when adding 4 and 5', () => { expect(add(4, 5)).toBe(9); }); ``` Esta prueba demuestra que devolver una constante no satisface el comportamiento general. La implementación debe evolucionar: ```typescript function add(a: number, b: number): number { return a + b; } ``` ### Criterios de triangulación - Añade ejemplos que aporten información nueva. - Incluye casos límite cuando sean relevantes. - Evita pruebas redundantes que no aumenten la confianza. - No generes combinaciones arbitrarias sin relación con los requisitos. - Utiliza la triangulación para descubrir comportamientos, no para retrasar artificialmente una solución evidente. La triangulación es especialmente útil cuando las reglas del dominio son complejas o todavía no están completamente definidas. --- ## 5. Prioridad de las transformaciones (Transformation Priority Premise) La *Transformation Priority Premise* propone avanzar desde implementaciones simples hacia otras más complejas mediante transformaciones pequeñas. Es una herramienta de aprendizaje y razonamiento, no un algoritmo obligatorio de implementación. | Prioridad orientativa | Transformación | |---|---| | 1 | Estructura vacía → ausencia de valor (`nil`/`null`) | | 2 | Ausencia de valor → constante | | 3 | Constante → variable | | 4 | Comportamiento incondicional → condicional | | 5 | Valor escalar → colección | | 6 | Instrucción → recursión | | 7 | Valor → valor modificado | ### Criterios de aplicación - Prefiere transformaciones pequeñas cuando reduzcan el riesgo. - No introduzcas complejidad antes de que resulte necesaria. - No fuerces una secuencia concreta de transformaciones. - Utiliza directamente una implementación evidente cuando sea más clara. - Evalúa la transformación según las características del lenguaje y del dominio. **La prioridad no debe interpretarse como una clasificación universal de complejidad o calidad.** --- ## 6. Patrón Preparar-Actuar-Verificar (Arrange-Act-Assert) La referencia canónica sobre AAA y nomenclatura es [testing.md](testing.md), secciones 3 y 4. Aquí se conserva su aplicación al ciclo TDD y al diseño de la prueba en orden inverso. El patrón AAA estructura las pruebas en tres fases: 1. **Preparar (Arrange):** establecer el estado y los datos necesarios. 2. **Actuar (Act):** ejecutar el comportamiento que se desea comprobar. 3. **Verificar (Assert):** comprobar el resultado esperado. ```typescript it('applies a 10 percent discount to the order total', () => { // Preparar: Crear el pedido y el descuento. const order = new Order(); order.addItem({ price: 100 }); const discount = new PercentDiscount(10); // Actuar: Calcular el importe final. const total = order.calculateTotal(discount); // Verificar: Comprobar el resultado. expect(total).toBe(90); }); ``` El ejemplo presupone que el dominio utiliza importes numéricos. En un sistema con reglas monetarias específicas puede ser preferible utilizar objetos de valor como `Money`. ### Buenas prácticas - Mantén claramente identificable el comportamiento probado. - Evita configuraciones extensas que oculten la intención. - Utiliza funciones auxiliares o factorías de pruebas cuando simplifiquen la preparación. - No introduzcas abstracciones de pruebas innecesarias. - Mantén las verificaciones relacionadas con el comportamiento esperado. No es obligatorio escribir comentarios para separar las tres fases cuando la estructura resulte evidente. --- ## 7. Escribir pruebas en orden inverso En ocasiones resulta útil diseñar una prueba comenzando por la verificación. El procedimiento consiste en: 1. **ASSERT:** definir qué resultado debe comprobarse. 2. **ACT:** identificar qué operación produce ese resultado. 3. **ARRANGE:** preparar únicamente los datos y dependencias necesarios. ### Ejemplo Primero, define la expectativa: ```typescript expect(total).toBe(90); ``` Después, identifica la operación: ```typescript const total = order.calculateTotal(discount); ``` Por último, prepara el escenario: ```typescript const order = new Order(); order.addItem({ price: 100 }); const discount = new PercentDiscount(10); ``` Esta técnica ayuda a evitar configuraciones innecesarias y favorece pruebas centradas en resultados observables. No es obligatorio utilizarla cuando el comportamiento ya esté suficientemente claro. --- ## 8. Nomenclatura de las pruebas Los nombres deben describir comportamientos concretos utilizando el lenguaje del dominio. ### Principios - Describe el comportamiento esperado. - Utiliza ejemplos concretos cuando aporten claridad. - Evita referencias innecesarias a detalles internos. - Mantén una relación clara entre el nombre y la verificación realizada. - Utiliza nombres coherentes con las convenciones del proyecto. - Facilita identificar el fallo sin necesidad de inspeccionar toda la prueba. ```typescript // MAL: Se centra en un detalle interno. it('sets the data property to 1', () => { // ... }); ``` ```typescript // BIEN: Describe un comportamiento observable. it('recognizes "mom" as a palindrome', () => { expect(isPalindrome('mom')).toBe(true); }); ``` ### Una responsabilidad de prueba por caso Como orientación, cada prueba debe verificar un comportamiento concreto. Esto no significa limitarla a una única instrucción `expect`. Una misma operación puede requerir comprobar varios efectos relacionados. ```typescript it('creates an order with the expected initial state', () => { const order = createOrder(); expect(order.status).toBe('pending'); expect(order.items).toHaveLength(0); expect(order.total).toBe(0); }); ``` Estas verificaciones pertenecen al mismo comportamiento inicial. Evita agrupar escenarios independientes dentro de una única prueba extensa. --- ## 9. TDD clásico y Mockist Existen diferentes estilos de TDD. Ninguno debe imponerse universalmente. ### 9.1. TDD clásico (Detroit/Chicago) Se centra principalmente en los estados y resultados observables. Características: - Prefiere utilizar objetos reales cuando resulta práctico. - Evita simulaciones innecesarias. - Comprueba resultados y cambios de estado. - Permite que el diseño evolucione mediante el comportamiento. Resulta especialmente apropiado para: - Funciones puras. - Objetos de dominio. - Algoritmos. - Reglas de negocio. - Componentes que pueden probarse sin infraestructura externa. El uso de objetos reales no convierte automáticamente una prueba en una prueba de integración. ### 9.2. TDD Mockist (London) Se centra especialmente en las interacciones y colaboración entre componentes. Características: - Utiliza dobles de prueba para sustituir determinadas dependencias. - Permite verificar colaboraciones relevantes. - Facilita aislar componentes. - Puede favorecer diseños basados en interfaces y responsabilidades explícitas. Resulta útil cuando: - Existen dependencias externas. - Deben verificarse interacciones relevantes. - Es necesario controlar respuestas o errores de colaboradores. - La infraestructura dificulta ejecutar pruebas rápidas y deterministas. ### Criterios para utilizar mocks - No simules dependencias únicamente por costumbre. - Evita simular objetos de dominio sencillos que puedan utilizarse directamente. - Utiliza dobles de prueba en los límites con bases de datos, servicios externos, sistemas de archivos u otras dependencias cuando resulte apropiado. - No hagas que las pruebas dependan excesivamente de llamadas internas irrelevantes. - Comprueba que las simulaciones respetan los contratos reales de sus dependencias. - Complementa las pruebas aisladas con pruebas de integración cuando sea necesario. **Una prueba que verifica demasiados detalles de implementación puede fallar ante una refactorización correcta, aunque el comportamiento observable no haya cambiado.** Selecciona el enfoque que proporcione mayor confianza con un coste razonable de mantenimiento. --- ## 10. TDD en código legado El código legado puede carecer de pruebas, presentar dependencias difíciles de aislar o contener comportamientos implícitos. En estos escenarios, no siempre resulta apropiado comenzar con una prueba unitaria convencional. ### Procedimiento recomendado 1. Identifica el comportamiento que debe modificarse. 2. Analiza sus consumidores, contratos y dependencias. 3. Comprueba qué pruebas y herramientas de verificación existen. 4. Cuando sea viable, escribe pruebas de caracterización para documentar el comportamiento actual. 5. Introduce los cambios de forma incremental. 6. Añade pruebas de regresión para el comportamiento corregido o incorporado. 7. Ejecuta las verificaciones disponibles. ### Pruebas de caracterización Una prueba de caracterización describe el comportamiento actual de un componente, aunque este no sea necesariamente el comportamiento ideal. Su objetivo es detectar cambios accidentales durante una refactorización. No debe utilizarse para convertir automáticamente un error conocido en un requisito permanente. Cuando el objetivo sea corregir un error, distingue entre: - Comportamiento existente que debe preservarse. - Comportamiento defectuoso que debe corregirse. - Comportamiento nuevo que debe incorporarse. --- ## 11. Pruebas de regresión Al corregir un defecto reproducible, es recomendable incorporar una prueba que detecte su reaparición. ### Procedimiento 1. Reproduce el defecto. 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 pruebas relacionadas para detectar regresiones. Evita modificar o eliminar pruebas existentes únicamente para conseguir que la nueva implementación pase. Si el comportamiento anterior era incorrecto y las pruebas lo reflejaban, actualízalas justificando el cambio de requisito o la corrección del defecto. --- ## 12. Errores habituales al aplicar TDD | Error | Criterio de corrección | |---|---| | Escribir código antes de una prueba durante un ciclo TDD | Recuperar el ciclo Rojo-Verde-Refactorizar. | | Escribir demasiadas pruebas antes de implementar | Trabajar en incrementos de comportamiento pequeños. | | Implementar más funcionalidad de la necesaria | Limitarse a los requisitos y ejemplos relevantes. | | Omitir sistemáticamente la refactorización | Revisar el diseño una vez superadas las pruebas. | | Probar detalles internos | Priorizar comportamientos y contratos observables. | | Utilizar nombres abstractos | Describir escenarios y resultados concretos. | | Extraer abstracciones demasiado pronto | Esperar a comprender la responsabilidad compartida. | | Aplicar literalmente la regla de tres | Evaluar duplicación de conocimiento y coste de mantenimiento. | | Abusar de mocks | Utilizar objetos reales cuando resulten más sencillos y fiables. | | Mantener implementaciones artificiales de *Fake It* | Generalizar cuando las pruebas y requisitos lo exijan. | | Confiar únicamente en pruebas unitarias | Incorporar pruebas de integración o E2E cuando corresponda. | | Modificar pruebas para ocultar defectos | Investigar el fallo y preservar los requisitos correctos. | | Exigir TDD en cualquier circunstancia | Seleccionar la estrategia de verificación adecuada al contexto. | --- ## 13. Lista de comprobación de TDD ### Antes de implementar - [ ] ¿Comprendo el comportamiento esperado? - [ ] ¿Existen criterios de aceptación claros? - [ ] ¿Qué nivel de prueba resulta más apropiado? - [ ] ¿Puedo escribir primero una prueba que reproduzca el comportamiento pendiente? - [ ] ¿Conozco el comportamiento existente que debo preservar? - [ ] ¿Puedo utilizar la infraestructura de pruebas del proyecto? ### Durante la implementación - [ ] ¿La prueba falla inicialmente por el motivo esperado? - [ ] ¿La implementación satisface el comportamiento requerido? - [ ] ¿Estoy evitando funcionalidades no solicitadas? - [ ] ¿Las pruebas siguen siendo deterministas? - [ ] ¿Estoy probando comportamientos en lugar de detalles internos? - [ ] ¿He evitado mocks y abstracciones innecesarias? ### Durante la refactorización - [ ] ¿Las pruebas continúan pasando? - [ ] ¿Se conserva el comportamiento observable? - [ ] ¿Las abstracciones introducidas representan conceptos reales? - [ ] ¿He mejorado la comprensión sin fragmentar artificialmente el código? - [ ] ¿He evitado cambios ajenos al alcance de la tarea? ### Al finalizar - [ ] ¿Se satisfacen los criterios de aceptación? - [ ] ¿Se han ejecutado las pruebas relevantes? - [ ] ¿Se han incorporado pruebas de regresión cuando corresponde? - [ ] ¿Se han verificado los contratos y comportamientos afectados? - [ ] ¿He identificado las pruebas que no pudieron ejecutarse? - [ ] ¿Puedo comunicar con precisión qué se ha verificado y qué permanece pendiente? --- ## Principio final > "TDD es una herramienta para descubrir y verificar comportamientos, no un objetivo independiente de la calidad del software." Utiliza el ciclo Rojo-Verde-Refactorizar cuando permita desarrollar de forma segura, incremental y comprensible. No conviertas las reglas metodológicas en restricciones que impidan adoptar una solución técnicamente más apropiada. **El objetivo es producir software correcto, verificable, sencillo y mantenible, utilizando las pruebas como una herramienta para mejorar tanto el diseño como la confianza en el comportamiento del sistema.**