Desarrollo basado en pruebas

Técnica de codificación que consiste en escribir repetidamente código de prueba y luego código de producción.

El desarrollo basado en pruebas ( TDD ) es una forma de escribir código que implica escribir un caso de prueba automatizado a nivel de unidad que falla, luego escribir solo el código suficiente para que la prueba pase, luego refactorizar tanto el código de prueba como el código de producción, y luego repetir con otro nuevo caso de prueba.

Los enfoques alternativos para escribir pruebas automatizadas son escribir todo el código de producción antes de comenzar con el código de prueba o escribir todo el código de prueba antes de comenzar con el código de producción. Con TDD, ambos se escriben juntos, lo que reduce los tiempos de depuración necesarios. [1]

TDD está relacionado con los conceptos de programación de prueba primero de la programación extrema , que comenzó en 1999, [2] pero más recientemente ha creado un interés más general por derecho propio. [3]

Los programadores también aplican el concepto para mejorar y depurar código heredado desarrollado con técnicas más antiguas. [4]

Historia

El ingeniero de software Kent Beck , a quien se le atribuye haber desarrollado o "redescubierto" [5] la técnica, afirmó en 2003 que TDD fomenta diseños simples e inspira confianza. [6]

La descripción original de TDD se encontraba en un antiguo libro sobre programación. Decía que se toma la cinta de entrada, se escribe manualmente la cinta de salida que se espera y luego se programa hasta que la cinta de salida real coincida con la salida esperada. Después de haber escrito el primer marco xUnit en Smalltalk, recordé haber leído esto y lo probé. Ese fue el origen de TDD para mí. Cuando describo TDD a programadores mayores, a menudo escucho: "Por supuesto. ¿De qué otra manera se podría programar?" Por lo tanto, me refiero a mi papel como "redescubridor" de TDD.

—  Kent Beck , "¿Por qué Kent Beck se refiere al 'redescubrimiento' del desarrollo basado en pruebas? ¿Cuál es la historia del desarrollo basado en pruebas antes del redescubrimiento de Kent Beck?" [7]

Ciclo de codificación

Una representación gráfica del ciclo de vida del desarrollo basado en pruebas

Los pasos de TDD varían un poco según el autor en cuanto a cantidad y descripción , pero en general son los siguientes. Se basan en el libro Test-Driven Development by Example [6] y en el artículo Canon TDD de Kent Beck [8] .

1. Enumere los escenarios para la nueva función
Enumere las variantes esperadas en el nuevo comportamiento. “Está el caso básico y luego qué sucede si este servicio agota el tiempo de espera y qué sucede si la clave aún no está en la base de datos y…” El desarrollador puede descubrir estas especificaciones preguntando sobre casos de uso e historias de usuario . Un beneficio clave de TDD es que hace que el desarrollador se concentre en los requisitos antes de escribir el código. Esto contrasta con la práctica habitual, donde las pruebas unitarias solo se escriben después del código.
2. Escribe una prueba para un elemento de la lista.
Escriba una prueba automatizada que se aprobaría si se cumple la variante en el nuevo comportamiento.
3. Ejecute todas las pruebas. La nueva prueba debería fallar , por las razones esperadas
Esto demuestra que realmente se necesita un nuevo código para la función deseada. Valida que el instrumento de prueba esté funcionando correctamente. Descarta la posibilidad de que la nueva prueba tenga fallas y siempre sea aprobada.
4. Escribe el código más simple que pase la nueva prueba.
Se aceptan códigos poco elegantes y de código rígido . El código se perfeccionará en el paso 6. No se debe agregar ningún código que no sea la funcionalidad probada.
5. Todas las pruebas deberían pasar ahora
Si alguna falla, corrija las pruebas fallidas con cambios mínimos hasta que todas pasen.
6. Refactorice según sea necesario mientras se asegura de que todas las pruebas sigan aprobándose
El código se refactoriza para facilitar su lectura y mantenimiento. En particular, los datos de prueba codificados de forma rígida se deben eliminar del código de producción. La ejecución del conjunto de pruebas después de cada refactorización garantiza que no se dañe ninguna funcionalidad existente. Ejemplos de refactorización:
Repetir
Repita el proceso, comenzando en el paso 2, con cada prueba de la lista hasta que todas las pruebas se hayan implementado y aprobado.

Cada prueba debe ser pequeña y las confirmaciones deben realizarse con frecuencia. Si el código nuevo no supera algunas pruebas, el programador puede deshacerlas o revertirlas en lugar de depurar excesivamente.

Al utilizar bibliotecas externas , es importante no escribir pruebas que sean tan pequeñas como para probar efectivamente solo la biblioteca en sí, [3] a menos que haya alguna razón para creer que la biblioteca tiene errores o no es lo suficientemente rica en funciones para satisfacer todas las necesidades del software en desarrollo.

Trabajo basado en pruebas

El TDD se ha adoptado fuera del desarrollo de software, tanto en equipos de productos como de servicios, como trabajo impulsado por pruebas . [9] Para que las pruebas sean exitosas, deben practicarse en los niveles micro y macro. Cada método en una clase, cada valor de datos de entrada, mensaje de registro y código de error, entre otros puntos de datos, deben probarse. [10] De manera similar al TDD, los equipos que no trabajan con software desarrollan controles de calidad (QC) (generalmente pruebas manuales en lugar de pruebas automatizadas) para cada aspecto del trabajo antes de comenzar. Estos controles de QC se utilizan luego para informar el diseño y validar los resultados asociados. Los seis pasos de la secuencia TDD se aplican con cambios semánticos menores:

  1. "Agregar una verificación" reemplaza "Agregar una prueba"
  2. "Ejecutar todas las comprobaciones" reemplaza "Ejecutar todas las pruebas"
  3. "Haz el trabajo" reemplaza a "Escribe algo de código"
  4. "Ejecutar todas las comprobaciones" reemplaza a "Ejecutar pruebas"
  5. "Limpiar el trabajo" reemplaza a "Refactorizar el código"
  6. "Repetir"

Estilo de desarrollo

Existen varios aspectos a tener en cuenta en el desarrollo basado en pruebas, por ejemplo, los principios de "manténlo simple, estúpido" ( KISS ) y " no lo vas a necesitar " (YAGNI). Al centrarse en escribir solo el código necesario para pasar las pruebas, los diseños a menudo pueden ser más limpios y claros que los que se logran con otros métodos. [6] En Test-Driven Development by Example , Kent Beck también sugiere el principio " finge hasta que lo logres ".

Para lograr un concepto de diseño avanzado, como un patrón de diseño , se escriben pruebas que generan ese diseño. El código puede seguir siendo más simple que el patrón de destino, pero aún así pasa todas las pruebas requeridas. Esto puede resultar desconcertante al principio, pero permite que el desarrollador se concentre solo en lo que es importante.

Escribir las pruebas primero: Las pruebas deben escribirse antes de la funcionalidad que se va a probar. Se ha afirmado que esto tiene muchos beneficios. Ayuda a garantizar que la aplicación esté escrita para que sea testeable, ya que los desarrolladores deben considerar cómo probar la aplicación desde el principio en lugar de agregarla más tarde. También garantiza que se escriban pruebas para cada característica. Además, escribir las pruebas primero conduce a una comprensión más profunda y temprana de los requisitos del producto, garantiza la efectividad del código de prueba y mantiene un enfoque continuo en la calidad del software . [11] Al escribir código que prioriza las características, existe una tendencia por parte de los desarrolladores y las organizaciones a empujar al desarrollador a la siguiente característica, incluso descuidando las pruebas por completo. Es posible que la primera prueba TDD ni siquiera se compile al principio, porque es posible que las clases y los métodos que requiere aún no existan. Sin embargo, esa primera prueba funciona como el comienzo de una especificación ejecutable. [12]

Cada caso de prueba falla inicialmente: esto garantiza que la prueba realmente funcione y pueda detectar un error. Una vez que esto se demuestra, se puede implementar la funcionalidad subyacente. Esto ha llevado al "mantra del desarrollo impulsado por pruebas", que es "rojo/verde/refactorización", donde rojo significa fallar y verde significa aprobar . El desarrollo impulsado por pruebas repite constantemente los pasos de agregar casos de prueba que fallan, aprobarlos y refactorizar. Recibir los resultados de prueba esperados en cada etapa refuerza el modelo mental del código del desarrollador, aumenta la confianza y aumenta la productividad.

Visibilidad del código

El código de prueba necesita acceso al código que está probando, pero la prueba no debe comprometer los objetivos de diseño normales, como el ocultamiento de información , la encapsulación y la separación de intereses . Por lo tanto, el código de prueba unitaria generalmente se encuentra en el mismo proyecto o módulo que el código que se está probando.

En el diseño orientado a objetos, esto todavía no proporciona acceso a datos y métodos privados. Por lo tanto, puede ser necesario un trabajo adicional para las pruebas unitarias. En Java y otros lenguajes, un desarrollador puede usar la reflexión para acceder a campos y métodos privados. [13] Alternativamente, se puede usar una clase interna para contener las pruebas unitarias de modo que tengan visibilidad de los miembros y atributos de la clase envolvente. En .NET Framework y algunos otros lenguajes de programación, se pueden usar clases parciales para exponer métodos y datos privados para que las pruebas accedan a ellos.

Es importante que estos trucos de prueba no permanezcan en el código de producción. En C y otros lenguajes, se pueden colocar directivas del compilador como #if DEBUG ... #endifalrededor de estas clases adicionales y, de hecho, de todo el código relacionado con las pruebas para evitar que se compilen en el código publicado. Esto significa que el código publicado no es exactamente el mismo que el que se probó unitariamente. La ejecución regular de menos pruebas de integración, pero más completas y de extremo a extremo, en la versión final puede garantizar (entre otras cosas) que no exista ningún código de producción que dependa sutilmente de aspectos del conjunto de pruebas.

Existe cierto debate entre los practicantes de TDD, documentado en sus blogs y otros escritos, sobre si es prudente probar métodos y datos privados de todos modos. Algunos sostienen que los miembros privados son un mero detalle de implementación que puede cambiar y que se les debería permitir hacerlo sin alterar el número de pruebas. Por lo tanto, debería ser suficiente probar cualquier clase a través de su interfaz pública o a través de su interfaz de subclase, que algunos lenguajes llaman la interfaz "protegida". [14] Otros dicen que los aspectos cruciales de la funcionalidad se pueden implementar en métodos privados y que probarlos directamente ofrece la ventaja de pruebas unitarias más pequeñas y directas. [15] [16]

Pruebas falsas, simuladas y de integración

Las pruebas unitarias se denominan así porque cada una prueba una unidad de código. Un módulo complejo puede tener mil pruebas unitarias y un módulo simple puede tener solo diez. Las pruebas unitarias utilizadas para TDD nunca deben cruzar los límites de proceso en un programa, y ​​mucho menos las conexiones de red. Hacerlo introduce demoras que hacen que las pruebas se ejecuten lentamente y desalientan a los desarrolladores a ejecutar todo el conjunto. La introducción de dependencias en módulos o datos externos también convierte las pruebas unitarias en pruebas de integración . Si un módulo se comporta mal en una cadena de módulos interrelacionados, no está tan claro de inmediato dónde buscar la causa del fallo.

Cuando el código en desarrollo depende de una base de datos, un servicio web o cualquier otro proceso o servicio externo, imponer una separación que permita realizar pruebas unitarias también es una oportunidad y una fuerza impulsora para diseñar un código más modular, más comprobable y más reutilizable. [17] Son necesarios dos pasos:

  1. Siempre que se necesite acceso externo en el diseño final, se debe definir una interfaz que describa el acceso disponible. Consulte el principio de inversión de dependencias para obtener una explicación de los beneficios de hacer esto independientemente de TDD.
  2. La interfaz debe implementarse de dos maneras, una de las cuales realmente accede al proceso externo y la otra es una falsa o simulada . Los objetos falsos necesitan hacer poco más que agregar un mensaje como "Objeto persona guardado" a un registro de seguimiento , contra el cual se puede ejecutar una aserción de prueba para verificar el comportamiento correcto. Los objetos simulados se diferencian en que ellos mismos contienen aserciones de prueba que pueden hacer que la prueba falle, por ejemplo, si el nombre de la persona y otros datos no son los esperados.

Los métodos de objetos falsos y simulados que devuelven datos, aparentemente de un almacén de datos o de un usuario, pueden ayudar al proceso de prueba al devolver siempre los mismos datos realistas en los que las pruebas pueden confiar. También se pueden configurar en modos de falla predefinidos para que se puedan desarrollar rutinas de manejo de errores y probarlas de manera confiable. En un modo de falla, un método puede devolver una respuesta inválida, incompleta o nula , o puede lanzar una excepción . Los servicios falsos que no sean almacenes de datos también pueden ser útiles en TDD: un servicio de cifrado falso puede, de hecho, no cifrar los datos pasados; un servicio de números aleatorios falso siempre puede devolver 1. Las implementaciones falsas o simuladas son ejemplos de inyección de dependencia .

Un doble de prueba es una capacidad específica de prueba que sustituye a una capacidad del sistema, normalmente una clase o función, de la que depende la UUT. Hay dos momentos en los que se pueden introducir dobles de prueba en un sistema: enlace y ejecución. La sustitución en el momento del enlace es cuando el doble de prueba se compila en el módulo de carga, que se ejecuta para validar la prueba. Este enfoque se utiliza normalmente cuando se ejecuta en un entorno distinto del entorno de destino que requiere dobles para el código de nivel de hardware para la compilación. La alternativa a la sustitución del enlazador es la sustitución en tiempo de ejecución en la que la funcionalidad real se reemplaza durante la ejecución de un caso de prueba. Esta sustitución se realiza normalmente mediante la reasignación de punteros de función conocidos o el reemplazo de objetos.

Los dobles de prueba son de distintos tipos y de variada complejidad:

  • Dummy : un dummy es la forma más simple de un doble de prueba. Facilita la sustitución en el momento del enlazador al proporcionar un valor de retorno predeterminado cuando es necesario.
  • Stub – Un stub agrega lógica simplista a un modelo ficticio, brindando diferentes salidas.
  • Espía: un espía captura y pone a disposición información de parámetros y estado, publicando accesores al código de prueba para obtener información privada, lo que permite una validación de estado más avanzada.
  • Simulacro : un simulacro se especifica mediante un caso de prueba individual para validar el comportamiento específico de la prueba, verificando los valores de los parámetros y la secuencia de llamadas.
  • Simulador: un simulador es un componente integral que proporciona una aproximación de mayor fidelidad de la capacidad objetivo (el objeto que se está duplicando). Un simulador normalmente requiere un esfuerzo de desarrollo adicional significativo. [11]

Un corolario de esta inyección de dependencia es que la base de datos real u otro código de acceso externo nunca es probado por el propio proceso TDD. Para evitar errores que pueden surgir de esto, se necesitan otras pruebas que instancian el código controlado por pruebas con las implementaciones "reales" de las interfaces analizadas anteriormente. Estas son pruebas de integración y son bastante independientes de las pruebas unitarias TDD. Hay menos de ellas y deben ejecutarse con menos frecuencia que las pruebas unitarias. No obstante, pueden implementarse utilizando el mismo marco de pruebas.

Las pruebas de integración que alteran cualquier base de datos o almacenamiento persistente siempre deben diseñarse cuidadosamente teniendo en cuenta el estado inicial y final de los archivos o la base de datos, incluso si alguna prueba falla. Esto se logra a menudo utilizando alguna combinación de las siguientes técnicas:

  • El TearDownmétodo que es parte integral de muchos marcos de prueba.
  • try...catch...finally estructuras de manejo de excepciones cuando estén disponibles.
  • Transacciones de base de datos donde una transacción incluye atómicamente quizás una operación de escritura, una de lectura y una de eliminación correspondiente.
  • Tomar una "instantánea" de la base de datos antes de ejecutar cualquier prueba y volver a la instantánea después de cada ejecución de prueba. Esto se puede automatizar utilizando un marco como Ant o NAnt o un sistema de integración continua como CruiseControl .
  • Inicializar la base de datos a un estado limpio antes de las pruebas, en lugar de limpiarla después de ellas. Esto puede ser relevante cuando la limpieza puede dificultar el diagnóstico de fallas de las pruebas al eliminar el estado final de la base de datos antes de que se pueda realizar un diagnóstico detallado.

Mantenga la unidad pequeña

En el caso de TDD, una unidad se define más comúnmente como una clase o un grupo de funciones relacionadas, a menudo denominado módulo. Se afirma que mantener las unidades relativamente pequeñas ofrece ventajas fundamentales, entre ellas:

  • Esfuerzo de depuración reducido: cuando se detectan fallas en las pruebas, tener unidades más pequeñas ayuda a rastrear los errores.
  • Pruebas autodocumentadas: los casos de prueba pequeños son más fáciles de leer y comprender. [11]

Las prácticas avanzadas de desarrollo basado en pruebas pueden llevar al desarrollo basado en pruebas de aceptación (ATDD, por sus siglas en inglés) y a la especificación por ejemplo, donde los criterios especificados por el cliente se automatizan en pruebas de aceptación, que luego impulsan el proceso tradicional de desarrollo basado en pruebas unitarias (UTDD, por sus siglas en inglés). [18] Este proceso garantiza que el cliente tenga un mecanismo automatizado para decidir si el software cumple con sus requisitos. Con ATDD, el equipo de desarrollo ahora tiene un objetivo específico que satisfacer (las pruebas de aceptación), lo que los mantiene continuamente enfocados en lo que el cliente realmente quiere de cada historia de usuario.

Mejores prácticas

Estructura de la prueba

Un diseño eficaz de un caso de prueba garantiza que se completen todas las acciones necesarias, mejora la legibilidad del caso de prueba y facilita el flujo de ejecución. Una estructura coherente ayuda a crear un caso de prueba autodocumentado. Una estructura que se aplica comúnmente para los casos de prueba tiene (1) configuración, (2) ejecución, (3) validación y (4) limpieza.

  • Configuración: Coloque la unidad bajo prueba (UUT) o el sistema de prueba general en el estado necesario para ejecutar la prueba.
  • Ejecución: activa o activa la UUT para que realice el comportamiento objetivo y capture todos los resultados, como los valores de retorno y los parámetros de salida. Este paso suele ser muy simple.
  • Validación: garantizar que los resultados de la prueba sean correctos. Estos resultados pueden incluir resultados explícitos capturados durante la ejecución o cambios de estado en la UUT.
  • Limpieza: Restaurar la UUT o el sistema de prueba general al estado previo a la prueba. Esta restauración permite ejecutar otra prueba inmediatamente después de esta. En algunos casos, para preservar la información para un posible análisis de fallas de la prueba, la limpieza debe comenzar la prueba justo antes de la ejecución de la configuración de la prueba. [11]

Mejores prácticas individuales

Algunas de las mejores prácticas que una persona podría seguir serían separar la lógica común de configuración y desmontaje en servicios de soporte de pruebas utilizados por los casos de prueba apropiados, para mantener cada oráculo de prueba enfocado solo en los resultados necesarios para validar su prueba y para diseñar pruebas relacionadas con el tiempo para permitir la tolerancia para la ejecución en sistemas operativos que no sean de tiempo real. La práctica común de permitir un margen de 5 a 10 por ciento para la ejecución tardía reduce la cantidad potencial de falsos negativos en la ejecución de pruebas. También se sugiere tratar el código de prueba con el mismo respeto que el código de producción. El código de prueba debe funcionar correctamente tanto para casos positivos como negativos, durar mucho tiempo y ser legible y mantenible. Los equipos pueden reunirse y revisar pruebas y prácticas de prueba para compartir técnicas efectivas y detectar malos hábitos. [19]

Prácticas a evitar o “antipatrones”

  • Hacer que los casos de prueba dependan del estado del sistema manipulado a partir de casos de prueba ejecutados previamente (es decir, siempre debe iniciar una prueba unitaria desde un estado conocido y preconfigurado).
  • Dependencias entre casos de prueba. Una suite de pruebas en la que los casos de prueba dependen unos de otros es frágil y compleja. No se debe presuponer el orden de ejecución. La refactorización básica de los casos de prueba iniciales o la estructura de la UUT provoca una espiral de impactos cada vez más generalizados en las pruebas asociadas.
  • Pruebas interdependientes. Las pruebas interdependientes pueden generar falsos negativos en cascada. Una falla en un caso de prueba inicial afecta a un caso de prueba posterior incluso si no existe una falla real en la prueba ejecutable, lo que aumenta los esfuerzos de análisis y depuración de defectos.
  • Probar la ejecución precisa, el comportamiento, el tiempo o el rendimiento.
  • Creación de "oráculos que todo lo saben". Un oráculo que inspecciona más de lo necesario es más costoso y frágil con el tiempo. Este error muy común es peligroso porque causa una pérdida de tiempo sutil pero generalizada en todo el complejo proyecto. [19] [ Aclaración necesaria ]
  • Detalles de implementación de pruebas.
  • Pruebas de ejecución lenta.

Comparación y demarcación

TDD y ATDD

El desarrollo impulsado por pruebas está relacionado con el desarrollo impulsado por pruebas de aceptación (ATDD), pero es diferente. [20] TDD es principalmente una herramienta para desarrolladores que ayuda a crear una unidad de código bien escrita (función, clase o módulo) que realice correctamente un conjunto de operaciones. ATDD es una herramienta de comunicación entre el cliente, el desarrollador y el evaluador para garantizar que los requisitos estén bien definidos. TDD requiere automatización de pruebas. ATDD no, aunque la automatización ayuda con las pruebas de regresión. Las pruebas utilizadas en TDD a menudo se pueden derivar de pruebas ATDD, ya que las unidades de código implementan alguna parte de un requisito. Las pruebas ATDD deben ser legibles por el cliente. Las pruebas TDD no necesitan serlo.

TDD y BDD

BDD ( desarrollo impulsado por el comportamiento ) combina prácticas de TDD y de ATDD. [21] Incluye la práctica de escribir pruebas primero, pero se centra en pruebas que describen el comportamiento, en lugar de pruebas que prueban una unidad de implementación. Herramientas como JBehave, Cucumber , Mspec y Specflow proporcionan sintaxis que permiten a los propietarios de productos, desarrolladores e ingenieros de pruebas definir juntos los comportamientos que luego se pueden traducir en pruebas automatizadas.

Software para TDD

Hay muchos marcos y herramientas de prueba que son útiles en TDD.

Marcos de trabajo xUnit

Los desarrolladores pueden utilizar marcos de prueba asistidos por computadora , comúnmente denominados colectivamente xUnit (que se derivan de SUnit, creado en 1998), para crear y ejecutar automáticamente los casos de prueba. Los marcos xUnit proporcionan capacidades de validación de pruebas de estilo de aserción e informes de resultados. Estas capacidades son fundamentales para la automatización, ya que trasladan la carga de la validación de la ejecución de una actividad de posprocesamiento independiente a una que se incluye en la ejecución de la prueba. El marco de ejecución proporcionado por estos marcos de prueba permite la ejecución automática de todos los casos de prueba del sistema o varios subconjuntos junto con otras características. [22]

Resultados del TAP

Los marcos de prueba pueden aceptar resultados de pruebas unitarias en el protocolo Test Anything, independiente del lenguaje, creado en 1987.

TDD para sistemas complejos

Para poner en práctica TDD en sistemas grandes y complejos se necesita una arquitectura modular, componentes bien definidos con interfaces publicadas y una estructura de sistemas disciplinada en capas que maximice la independencia de la plataforma. Estas prácticas comprobadas permiten aumentar la capacidad de prueba y facilitan la aplicación de la automatización de pruebas y compilaciones. [11]

Diseño para la capacidad de prueba

Los sistemas complejos requieren una arquitectura que cumpla con una serie de requisitos. Un subconjunto clave de estos requisitos incluye el soporte para la prueba completa y eficaz del sistema. Un diseño modular eficaz produce componentes que comparten características esenciales para un TDD eficaz.

  • La alta cohesión garantiza que cada unidad proporcione un conjunto de capacidades relacionadas y hace que las pruebas de esas capacidades sean más fáciles de mantener.
  • El bajo acoplamiento permite probar eficazmente cada unidad de forma aislada.
  • Las interfaces publicadas restringen el acceso a los componentes y sirven como puntos de contacto para las pruebas, lo que facilita la creación de pruebas y garantiza la máxima fidelidad entre la configuración de la unidad de prueba y la de producción.

Una técnica clave para construir una arquitectura modular eficaz es el modelado de escenarios, en el que se construye un conjunto de diagramas de secuencia, cada uno de ellos centrado en un único escenario de ejecución a nivel de sistema. El modelo de escenarios proporciona un vehículo excelente para crear la estrategia de interacciones entre componentes en respuesta a un estímulo específico. Cada uno de estos modelos de escenarios sirve como un conjunto completo de requisitos para los servicios o funciones que un componente debe proporcionar, y también dicta el orden en el que estos componentes y servicios interactúan entre sí. El modelado de escenarios puede facilitar en gran medida la construcción de pruebas TDD para un sistema complejo. [11]

Gestión de pruebas para equipos grandes

En un sistema más grande, el impacto de la mala calidad de los componentes se ve magnificado por la complejidad de las interacciones. Esta magnificación hace que los beneficios de TDD se acumulen incluso más rápido en el contexto de proyectos más grandes. Sin embargo, la complejidad de la población total de pruebas puede convertirse en un problema en sí misma, erosionando las ganancias potenciales. Parece simple, pero un paso inicial clave es reconocer que el código de prueba también es software importante y debe producirse y mantenerse con el mismo rigor que el código de producción.

La creación y gestión de la arquitectura del software de prueba dentro de un sistema complejo es tan importante como la arquitectura del producto principal. Los controladores de prueba interactúan con la UUT, los dobles de prueba y el marco de pruebas unitarias. [11]

Ventajas y desventajas del desarrollo basado en pruebas

Ventajas

El desarrollo guiado por pruebas (TDD) es un enfoque de desarrollo de software en el que las pruebas se escriben antes que el código real. Ofrece varias ventajas:

  1. Cobertura de pruebas integral : TDD garantiza que todo el código nuevo esté cubierto por al menos una prueba, lo que genera un software más sólido.
  2. Mayor confianza en el código : los desarrolladores adquieren mayor confianza en la confiabilidad y funcionalidad del código.
  3. Código bien documentado : el proceso naturalmente da como resultado un código bien documentado, ya que cada prueba aclara el propósito del código que prueba.
  4. Claridad de requisitos : TDD fomenta una comprensión clara de los requisitos antes de comenzar la codificación.
  5. Facilita la integración continua : se integra bien con los procesos de integración continua, lo que permite realizar actualizaciones y pruebas frecuentes del código.
  6. Aumenta la productividad : muchos desarrolladores descubren que TDD aumenta su productividad.
  7. Refuerza el modelo mental del código : TDD ayuda a construir un modelo mental sólido de la estructura y el comportamiento del código.
  8. Énfasis en el diseño y la funcionalidad : fomenta un enfoque en el diseño, la interfaz y la funcionalidad general del programa.
  9. Reduce la necesidad de depuración : al detectar problemas en una etapa temprana del proceso de desarrollo, TDD reduce la necesidad de realizar una depuración exhaustiva más adelante.
  10. Estabilidad del sistema : las aplicaciones desarrolladas con TDD tienden a ser más estables y menos propensas a errores. [23]

Desventajas

Sin embargo, el TDD no está exento de inconvenientes:

  1. Mayor volumen de código : la implementación de TDD puede generar una base de código más grande ya que las pruebas se suman a la cantidad total de código escrito.
  2. Falsa seguridad de las pruebas : una gran cantidad de pruebas aprobadas a veces puede dar una sensación engañosa de seguridad respecto de la solidez del código. [24]
  3. Gastos generales de mantenimiento : mantener un conjunto grande de pruebas puede agregar gastos generales al proceso de desarrollo.
  4. Procesos de prueba que consumen mucho tiempo : escribir y mantener pruebas puede consumir mucho tiempo.
  5. Configuración del entorno de prueba : TDD requiere configurar y mantener un entorno de prueba adecuado.
  6. Curva de aprendizaje : se necesita tiempo y esfuerzo para dominar las prácticas de TDD.
  7. Complicación excesiva : un énfasis excesivo en TDD puede generar un código más complejo de lo necesario.
  8. Descuido del diseño general : centrarse demasiado en aprobar pruebas a veces puede llevar a descuidar el panorama general del diseño de software.
  9. Costos mayores : el tiempo y los recursos adicionales requeridos para TDD pueden resultar en costos de desarrollo más altos.

Beneficios

Un estudio de 2005 concluyó que el uso de TDD implicaba escribir más pruebas y, a su vez, los programadores que escribían más pruebas tendían a ser más productivos. [25] Las hipótesis relacionadas con la calidad del código y una correlación más directa entre TDD y productividad no fueron concluyentes. [26]

Los programadores que utilizan TDD puro en proyectos nuevos (" greenfield ") informaron que rara vez sintieron la necesidad de invocar un depurador . Utilizado junto con un sistema de control de versiones , cuando las pruebas fallan inesperadamente, revertir el código a la última versión que pasó todas las pruebas puede ser a menudo más productivo que la depuración. [27]

El desarrollo basado en pruebas ofrece más que una simple validación de la corrección, sino que también puede guiar el diseño de un programa. [28] Al centrarse primero en los casos de prueba, uno debe imaginar cómo los clientes utilizan la funcionalidad (en el primer caso, los casos de prueba). Por lo tanto, el programador se preocupa por la interfaz antes de la implementación. Este beneficio es complementario al diseño por contrato , ya que aborda el código a través de casos de prueba en lugar de a través de afirmaciones matemáticas o preconcepciones.

El desarrollo basado en pruebas ofrece la posibilidad de realizar pequeños pasos cuando es necesario. Permite que el programador se centre en la tarea en cuestión, ya que el primer objetivo es que la prueba se apruebe. Los casos excepcionales y el manejo de errores no se tienen en cuenta inicialmente, y las pruebas para crear estas circunstancias ajenas se implementan por separado. De esta manera, el desarrollo basado en pruebas garantiza que todo el código escrito esté cubierto por al menos una prueba. Esto proporciona al equipo de programación y a los usuarios posteriores un mayor nivel de confianza en el código.

Si bien es cierto que se requiere más código con TDD que sin TDD debido al código de prueba unitaria, el tiempo total de implementación del código podría ser más corto según un modelo de Müller y Padberg. [29] Una gran cantidad de pruebas ayuda a limitar la cantidad de defectos en el código. La naturaleza temprana y frecuente de las pruebas ayuda a detectar defectos en las primeras etapas del ciclo de desarrollo, lo que evita que se conviertan en problemas endémicos y costosos. La eliminación de defectos en las primeras etapas del proceso generalmente evita una depuración prolongada y tediosa más adelante en el proyecto.

El TDD puede generar un código más modularizado, flexible y extensible. Este efecto suele producirse porque la metodología requiere que los desarrolladores piensen en el software en términos de pequeñas unidades que se pueden escribir y probar de forma independiente e integrar posteriormente. Esto genera clases más pequeñas y centradas, un acoplamiento más flexible e interfaces más claras. El uso del patrón de diseño de objetos simulados también contribuye a la modularización general del código porque este patrón requiere que el código se escriba de forma que los módulos se puedan cambiar fácilmente entre versiones simuladas para pruebas unitarias y versiones "reales" para la implementación.

Como no se escribe más código del necesario para superar un caso de prueba fallido, las pruebas automatizadas tienden a cubrir todas las rutas de código. Por ejemplo, para que un desarrollador de TDD añada una elserama a una ifdeclaración existente, primero tendría que escribir un caso de prueba fallido que motive la rama. Como resultado, las pruebas automatizadas resultantes de TDD tienden a ser muy exhaustivas: detectan cualquier cambio inesperado en el comportamiento del código. Esto detecta problemas que pueden surgir cuando un cambio posterior en el ciclo de desarrollo altera inesperadamente otra funcionalidad.

Madeyski [30] proporcionó evidencia empírica (a través de una serie de experimentos de laboratorio con más de 200 desarrolladores) con respecto a la superioridad de la práctica TDD sobre el enfoque tradicional Test-Last o el enfoque de prueba de corrección, con respecto al menor acoplamiento entre objetos (CBO). El tamaño del efecto medio representa un efecto mediano (pero cercano a grande) sobre la base del metanálisis de los experimentos realizados, lo que es un hallazgo sustancial. Sugiere una mejor modularización (es decir, un diseño más modular), una reutilización y prueba más sencillas de los productos de software desarrollados debido a la práctica de programación TDD. [30] Madeyski también midió el efecto de la práctica TDD en las pruebas unitarias utilizando la cobertura de rama (BC) y el indicador de puntuación de mutación (MSI), [31] [32] [33] que son indicadores de la minuciosidad y la eficacia de detección de fallas de las pruebas unitarias, respectivamente. El tamaño del efecto de TDD en la cobertura de rama fue de tamaño mediano y, por lo tanto, se considera un efecto sustancial. [30] Estos hallazgos se han confirmado posteriormente mediante evaluaciones experimentales adicionales y más pequeñas de TDD. [34] [35] [36] [37]

Beneficios psicológicos para el programador

  1. Mayor confianza : TDD permite a los programadores realizar cambios o agregar nuevas funciones con confianza. Saber que el código se prueba constantemente reduce el miedo a romper la funcionalidad existente. Esta red de seguridad puede fomentar enfoques más innovadores y creativos para la resolución de problemas.
  2. Menos miedo al cambio y menos estrés : en el desarrollo tradicional, cambiar el código existente puede resultar abrumador debido al riesgo de introducir errores. TDD, con su conjunto de pruebas integral, reduce este miedo, ya que las pruebas revelarán de inmediato cualquier problema causado por los cambios. Saber que la base de código tiene una red de seguridad de pruebas puede reducir el estrés y la ansiedad asociados con la programación. Los desarrolladores pueden sentirse más relajados y abiertos a experimentar y refactorizar.
  3. Enfoque mejorado : escribir pruebas primero ayuda a los programadores a concentrarse en los requisitos y el diseño antes de escribir el código. Este enfoque puede generar una codificación más clara y con un propósito más definido, ya que el desarrollador siempre es consciente del objetivo que intenta alcanzar.
  4. Sentimiento de logro y satisfacción laboral : aprobar exámenes puede brindar una sensación rápida y regular de logro, lo que eleva la moral. Esto puede ser particularmente motivador en proyectos a largo plazo donde el objetivo final puede parecer lejano. La combinación de todos estos factores puede generar una mayor satisfacción laboral. Cuando los desarrolladores se sienten seguros, concentrados y parte de un equipo colaborativo, su satisfacción laboral general puede mejorar significativamente.

Limitaciones

El desarrollo basado en pruebas no realiza pruebas suficientes en situaciones en las que se requieren pruebas funcionales completas para determinar el éxito o el fracaso, debido al uso extensivo de pruebas unitarias. [38] Ejemplos de estos son las interfaces de usuario , los programas que funcionan con bases de datos y algunos que dependen de configuraciones de red específicas . TDD alienta a los desarrolladores a colocar la cantidad mínima de código en dichos módulos y a maximizar la lógica que se encuentra en el código de biblioteca comprobable, utilizando falsificaciones y simulaciones para representar el mundo exterior. [39]

El apoyo de la dirección es esencial. Si toda la organización no cree que el desarrollo basado en pruebas va a mejorar el producto, la dirección puede pensar que el tiempo que se dedica a escribir pruebas es una pérdida de tiempo. [40]

Las pruebas unitarias creadas en un entorno de desarrollo basado en pruebas suelen ser creadas por el desarrollador que escribe el código que se está probando. Por lo tanto, las pruebas pueden compartir puntos ciegos con el código: si, por ejemplo, un desarrollador no se da cuenta de que se deben verificar ciertos parámetros de entrada, lo más probable es que ni la prueba ni el código verifiquen esos parámetros. Otro ejemplo: si el desarrollador malinterpreta los requisitos del módulo que está desarrollando, el código y las pruebas unitarias que escriba serán incorrectos de la misma manera. Por lo tanto, las pruebas pasarán, lo que dará una falsa sensación de corrección.

Una gran cantidad de pruebas unitarias aprobadas puede generar una falsa sensación de seguridad, lo que resulta en menos actividades de pruebas de software adicionales , como pruebas de integración y pruebas de cumplimiento .

Las pruebas se convierten en parte de la sobrecarga de mantenimiento de un proyecto. Las pruebas mal escritas, por ejemplo, las que incluyen cadenas de error codificadas de forma rígida, son propensas a fallar y su mantenimiento es costoso. Esto es especialmente cierto en el caso de las pruebas frágiles. [41] Existe el riesgo de que las pruebas que generan errores falsos con regularidad se ignoren, de modo que cuando se produzca un error real, es posible que no se detecte. Es posible escribir pruebas que requieran poco y fácil mantenimiento, por ejemplo, mediante la reutilización de cadenas de error, y esto debería ser un objetivo durante la fase de refactorización del código descrita anteriormente.

Escribir y mantener una cantidad excesiva de pruebas requiere tiempo. Además, los módulos más flexibles (con pruebas limitadas) pueden aceptar nuevos requisitos sin necesidad de cambiar las pruebas. Por esos motivos, las pruebas para condiciones extremas o una muestra pequeña de datos pueden ser más fáciles de ajustar que un conjunto de pruebas muy detalladas.

El nivel de cobertura y detalle de las pruebas que se logra durante los ciclos repetidos de TDD no se puede recrear fácilmente en una fecha posterior. Por lo tanto, estas pruebas originales o tempranas se vuelven cada vez más valiosas a medida que pasa el tiempo. La táctica es solucionarlo pronto. Además, si una arquitectura deficiente, un diseño deficiente o una estrategia de pruebas deficiente conducen a un cambio tardío que hace que decenas de pruebas existentes fallen, entonces es importante que se solucionen individualmente. Simplemente eliminarlas, deshabilitarlas o alterarlas precipitadamente puede generar fallas indetectables en la cobertura de las pruebas.

Conferencia

La primera conferencia TDD se celebró durante julio de 2021. [42] Las conferencias se grabaron en YouTube [43]

Véase también

Referencias

  1. ^ Parsa, Saeed; Zakeri-Nasrabadi, Morteza; Turhan, Burak (1 de enero de 2025). "Desarrollo impulsado por la capacidad de prueba: una mejora en la eficiencia de TDD". Estándares e interfaces informáticos . 91 : 103877. doi :10.1016/j.csi.2024.103877. ISSN  0920-5489.
  2. ^ Lee Copeland (diciembre de 2001). «Programación extrema». Computerworld. Archivado desde el original el 5 de junio de 2011. Consultado el 11 de enero de 2011 .
  3. ^ ab Newkirk, JW y Vorontsov, AA. Desarrollo basado en pruebas en Microsoft .NET , Microsoft Press, 2004.
  4. ^ Feathers, M. Cómo trabajar eficazmente con código heredado, Prentice Hall, 2004
  5. ^ Kent Beck (11 de mayo de 2012). "¿Por qué Kent Beck habla del "redescubrimiento" del desarrollo basado en pruebas?" . Consultado el 1 de diciembre de 2014 .
  6. ^ abc Beck, Kent (8 de noviembre de 2002). Desarrollo basado en pruebas mediante ejemplos . Vaseem: Addison Wesley. ISBN 978-0-321-14653-3.
  7. ^ Kent Beck (11 de mayo de 2012). "¿Por qué Kent Beck habla del "redescubrimiento" del desarrollo basado en pruebas?" . Consultado el 1 de diciembre de 2014 .
  8. ^ Beck, Kent (11 de diciembre de 2023). "Canon TDD". Diseño de software: ¿Tidy First? . Consultado el 22 de octubre de 2024 .
  9. ^ Leybourn, E. (2013) Dirigir la organización ágil: un enfoque Lean para la gestión empresarial . Londres: IT Governance Publishing: 176-179.
  10. ^ Mohan, Gayathri. "Pruebas de pila completa". www.thoughtworks.com . Consultado el 7 de septiembre de 2022 .
  11. ^ abcdefg "Informe técnico sobre TDD eficaz para sistemas integrados complejos" (PDF) . Pathfinder Solutions. Archivado desde el original (PDF) el 16 de marzo de 2016.
  12. ^ "Desarrollo ágil basado en pruebas". Agile Sherpa. 3 de agosto de 2010. Archivado desde el original el 23 de julio de 2012. Consultado el 14 de agosto de 2012 .
  13. ^ Burton, Ross (12 de noviembre de 2003). "Subvertir la protección de acceso de Java para pruebas unitarias". O'Reilly Media, Inc. . Consultado el 12 de agosto de 2009 .
  14. ^ van Rossum, Guido; Warsaw, Barry (5 de julio de 2001). «PEP 8: Guía de estilo para código Python». Python Software Foundation . Consultado el 6 de mayo de 2012 .
  15. ^ Newkirk, James (7 de junio de 2004). "Prueba de métodos privados/variables miembro: ¿debería hacerlo o no?". Microsoft Corporation . Consultado el 12 de agosto de 2009 .
  16. ^ Stall, Tim (1 de marzo de 2005). "Cómo probar métodos privados y protegidos en .NET". CodeProject . Consultado el 12 de agosto de 2009 .
  17. ^ Fowler, Martin (1999). Refactorización: mejora del diseño del código existente. Boston: Addison Wesley Longman, Inc. ISBN 0-201-48567-2.
  18. ^ Koskela, L. "Controlado por pruebas: TDD y TDD de aceptación para desarrolladores de Java", Manning Publications, 2007
  19. ^ Introducción al desarrollo basado en pruebas (TDD) para sistemas complejos en YouTube de Pathfinder Solutions
  20. ^ Desarrollo basado en pruebas de aceptación ágil y esbelta: mejor software mediante la colaboración . Boston: Addison Wesley Professional. 2011. ISBN 978-0321714084.
  21. ^ "BDD". Archivado desde el original el 8 de mayo de 2015. Consultado el 28 de abril de 2015 .
  22. ^ "Informe técnico sobre TDD eficaz para sistemas integrados complejos". Pathfinder Solutions. Archivado desde el original el 20 de agosto de 2013. Consultado el 27 de noviembre de 2012 .
  23. ^ Ventajas y desventajas del desarrollo basado en pruebas - LASOFT
  24. ^ Parsa, Saeed; Zakeri-Nasrabadi, Morteza; Turhan, Burak (1 de enero de 2025). "Desarrollo impulsado por la capacidad de prueba: una mejora en la eficiencia de TDD". Estándares e interfaces informáticos . 91 : 103877. doi :10.1016/j.csi.2024.103877. ISSN  0920-5489.
  25. ^ Erdogmus, Hakan; Morisio, Torchiano. "Sobre la eficacia del enfoque de prueba primero en la programación". Actas de IEEE Transactions on Software Engineering, 31(1). Enero de 2005. (NRC 47445). Archivado desde el original el 22 de diciembre de 2014. Consultado el 14 de enero de 2008. Descubrimos que los estudiantes que primero hacían pruebas, en promedio, escribieron más pruebas y, a su vez, los estudiantes que escribieron más pruebas tendían a ser más productivos.
  26. ^ Proffitt, Jacob. "TDD Proven Effective! Or is it?". Archivado desde el original el 2008-02-06 . Consultado el 2008-02-21 . Por lo tanto, la relación de TDD con la calidad es problemática en el mejor de los casos. Su relación con la productividad es más interesante. Espero que haya un estudio de seguimiento porque las cifras de productividad simplemente no me cuadran muy bien. Existe una correlación innegable entre la productividad y la cantidad de pruebas, pero esa correlación es en realidad más fuerte en el grupo sin TDD (que tuvo un solo valor atípico en comparación con aproximadamente la mitad del grupo TDD que estaba fuera de la banda del 95%).
  27. ^ Llopis, Noel (20 de febrero de 2005). "Pasando por el espejo: desarrollo de juegos basado en pruebas (parte 1)". Juegos desde dentro . Consultado el 1 de noviembre de 2007. Si comparamos [TDD] con el enfoque de desarrollo no basado en pruebas, estamos reemplazando toda la comprobación mental y el depurador por código que verifica que nuestro programa hace exactamente lo que pretendíamos que hiciera.
  28. ^ Mayr, Herwig (2005). Projekt Engineering Ingenieurmässige Softwareentwicklung in Projektgruppen (2., neu bearb. Aufl. ed.). Múnich: Fachbuchverl. Leipzig en Carl-Hanser-Verl. pag. 239.ISBN 978-3446400702.
  29. ^ Müller, Matthias M.; Padberg, Frank. "Acerca del retorno de la inversión en desarrollo basado en pruebas" (PDF) . Universität Karlsruhe, Alemania. pág. 6. S2CID  13905442. Archivado desde el original (PDF) el 2017-11-08 . Consultado el 2012-06-14 .
  30. ^ abc Madeyski, L. "Desarrollo basado en pruebas: una evaluación empírica de la práctica ágil", Springer, 2010, ISBN 978-3-642-04287-4 , págs. 1-245. DOI: 978-3-642-04288-1 
  31. ^ El impacto de la programación Test-First en la cobertura de ramas y el indicador de puntuación de mutación de pruebas unitarias: un experimento. por L. Madeyski Information & Software Technology 52(2): 169-184 (2010)
  32. ^ Sobre los efectos de la programación en pares en la minuciosidad y la eficacia de detección de fallos de las pruebas unitarias por L. Madeyski PROFES 2007: 207-221
  33. ^ Impacto de la programación en pares en la minuciosidad y la eficacia de la detección de fallos de los conjuntos de pruebas unitarias. por L. Madeyski Software Process: Improvement and Practice 13(3): 281-295 (2008)
  34. ^ M. Pančur y M. Ciglarič, "Impacto del desarrollo basado en pruebas en la productividad, el código y las pruebas: un experimento controlado", Information and Software Technology, 2011, vol. 53, núm. 6, págs. 557–573, DOI: 10.1016/j.infsof.2011.02.002
  35. ^ D. Fucci, H. Erdogmus, B. Turhan, M. Oivo y N. Juristo, "Una disección del proceso de desarrollo basado en pruebas: ¿realmente importa probar primero o probar al final?", IEEE Transactions on Software Engineering, 2017, vol. 43, n.º 7, págs. 597–614, DOI: 10.1109/TSE.2016.2616877
  36. ^ A. Tosun, O. Dieste Tubio, D. Fucci, S. Vegas, B. Turhan, H. Erdogmus, A. Santos, M. Oivo, K. Toro, J. Jarvinen y N. Juristo, "Un experimento industrial sobre los efectos del desarrollo basado en pruebas en la calidad externa y la productividad", Empirical Software Engineering, 2016, vol. 22, págs. 1–43, DOI: 10.1007/s10664-016-9490-0
  37. ^ B. Papis, K. Grochowski, K. Subzda y K. Sijko, "Evaluación experimental del desarrollo basado en pruebas con pasantes que trabajan en un proyecto industrial real", IEEE Transactions on Software Engineering, 2020, DOI: 10.1109/TSE.2020.3027522
  38. ^ "Problemas con TDD". Dalkescientific.com. 2009-12-29 . Consultado el 2014-03-25 .
  39. ^ Hunter, Andrew (19 de octubre de 2012). "¿Se utilizan demasiado las pruebas unitarias?". Simple-talk.com . Consultado el 25 de marzo de 2014 .
  40. ^ Loughran, Steve (6 de noviembre de 2006). "Testing" (PDF) . HP Laboratories . Consultado el 12 de agosto de 2009 .
  41. ^ "Pruebas frágiles".
  42. ^ Bunardzic, Alex. "Primera Conferencia Internacional sobre Desarrollo Basado en Pruebas (TDD)". Conferencia TDD . Consultado el 20 de julio de 2021 .
  43. ^ Primera Conferencia Internacional TDD - Sábado 10 de julio de 2021, 10 de julio de 2021, archivado desde el original el 2021-12-21 , consultado el 20 de julio de 2021
  • Desarrollo TestDriven en WikiWikiWeb
  • Bertrand Meyer (septiembre de 2004). "¿Prueba o especificación? ¿Prueba y especificación? ¡Prueba a partir de la especificación!". Archivado desde el original el 9 de febrero de 2005.
  • Prueba de equipo de Microsoft Visual Studio desde un enfoque TDD
  • Escriba pruebas unitarias fáciles de mantener que le ahorrarán tiempo y lágrimas
  • Mejorar la calidad de las aplicaciones mediante el desarrollo basado en pruebas (TDD)
  • Conferencia sobre desarrollo basado en pruebas
Retrieved from "https://en.wikipedia.org/w/index.php?title=Test-driven_development&oldid=1252779773"