Primero la prueba?

Cuando empecé con la programación dirigida por pruebas hace más de 10 años, conocía muchos conceptos diferentes en teoría. Pero este enfoque de escribir primero los casos de prueba y luego implementarlos no era, de alguna manera, la forma con la que me llevaba bien. Para ser honesto, este sigue siendo el caso hoy en día. Así que encontré una adaptación del paradigma TDD de Kent Beck que me funciona. Pero lo primero es lo primero. Quizás mi enfoque también sea bastante útil para unos u otros.

Originalmente provengo de entornos para aplicaciones web altamente escalables a las que todas las grandes teorías de la universidad no pueden aplicarse fácilmente en la práctica. La razón principal es la gran complejidad de dichas aplicaciones. Por un lado, varios sistemas adicionales como la caché en memoria, la base de datos y la gestión de identidades y accesos (IAM) forman parte del sistema global. Por otro lado, muchos marcos modernos como OR Mapper ocultan la complejidad tras diferentes capas de acceso. Como desarrolladores, necesitamos dominar todas estas cosas. Por eso existen soluciones robustas y probadas en la práctica que son bien conocidas pero que rara vez se utilizan. Kent Beck es una de las voces más importantes a favor del uso práctico de las pruebas automatizadas de software.

Si queremos involucrarnos en el concepto de TDD, es importante no dar demasiada importancia a cada personaje. No todo está escrito en piedra. Lo importante es el resultado al final del día. Por ello, es esencial tener presente el objetivo de todos los esfuerzos para lograr un valor añadido personal. Así que empecemos por ver qué queremos conseguir en primer lugar.

El éxito nos da la razón

Cuando empecé como desarrollador, necesitaba un feedback constante sobre si lo que estaba montando funcionaba realmente. La mayoría de las veces generaba esta retroalimentación esparciendo durante mi implementación innumerables salidas de consola, por un lado, y por el otro siempre intentaba integrar todo en una interfaz de usuario y luego ‘hacer clic’ manualmente. Básicamente una configuración de prueba muy engorrosa, que luego tiene que ser eliminada de nuevo al final. Si más tarde había que hacer correcciones de errores, todo el procedimiento comenzaba de nuevo. Todo resultaba en cierto modo insatisfactorio y muy alejado de una forma productiva de trabajar. Había que mejorar esto de alguna manera sin tener que reinventarse cada vez.

Por último, mi enfoque original tiene exactamente dos puntos débiles importantes. El más obvio es la entrada y salida de información de depuración a través de la consola.

Pero el segundo punto es mucho más grave. Porque todo el conocimiento adquirido sobre esta implementación en particular no se conserva. Por lo tanto, corre el riesgo de desvanecerse con el tiempo y, en última instancia, perderse. Sin embargo, estos conocimientos especializados son muy valiosos para muchas fases posteriores del proceso de desarrollo de software. Con esto me refiero explícitamente al tema de la calidad. La refactorización, las revisiones de código, las correcciones de errores y las solicitudes de cambio son sólo algunos de los posibles ejemplos en los que se requieren conocimientos detallados en profundidad.

Personalmente, el trabajo monótono y repetitivo me cansa rápidamente y me gustaría evitarlo. Pasar una y otra vez por una aplicación con el mismo procedimiento de prueba está muy lejos de lo que para mí constituye una jornada laboral satisfactoria. Quiero descubrir cosas nuevas. Pero sólo puedo hacerlo si no estoy atrapado en el pasado.

Conference Talk

Pero se atreven a hacer algo

Pero antes de entrar en cómo he condimentado mi trabajo diario de desarrollo con TDD, tengo que decir unas palabras sobre la responsabilidad y la valentía. En conversaciones, otras personas me han dicho con frecuencia que tengo razón, pero que no pueden tomar medidas para seguir mis recomendaciones porque el jefe de proyecto o algún otro superior no da luz verde.

Esa actitud me parece muy poco profesional. No le pregunto a un director de marketing qué algoritmo termina siendo el mejor. Simplemente no tiene ni idea de lo que estoy hablando, porque no es su área de responsabilidad. Un jefe de proyecto que hable en contra del trabajo basado en pruebas en el equipo de desarrollo también ha perdido su trabajo. Hoy en día, los marcos de pruebas están tan bien integrados en el entorno de compilación que incluso las personas sin experiencia pueden preparar TDD en cuestión de momentos. Por lo tanto, no es necesario darle demasiada importancia. Puedo prometer que incluso los primeros intentos no llevarán más tiempo que con el enfoque original. Al contrario, habrá un notable aumento de la productividad muy rápidamente.

La primera etapa de la evolución

Como ya he mencionado, el registro es una parte central del desarrollo basado en pruebas para mí. Siempre que tiene sentido, intento mostrar el estado de los objetos o variables en la consola. Si utilizamos los medios que nos proporciona el lenguaje de programación empleado para ello, esto significa que al menos debemos comentar esta salida del sistema una vez realizado el trabajo y volver a comentarla más tarde cuando busquemos errores. Un procedimiento redundante y propenso a errores.

Si, por el contrario, utilizamos un marco de logging desde el principio, podemos dejar tranquilamente la información de depuración en el código y desactivarla más tarde en el funcionamiento productivo mediante la configuración del nivel de log.

También utilizo el logging como trazador. Esto significa que cada constructor de una clase escribe una entrada de registro correspondiente por la información de nivel de registro mientras está siendo llamado. Esto me permite ver el orden en que se instancian los objetos. De vez en cuando también me he dado cuenta de la instanciación excesivamente frecuente de un mismo objeto. Esto es útil para las medidas de rendimiento y optimización de memoria.

Registro los errores que se lanzan durante el manejo de excepciones como errores o advertencias, dependiendo del contexto. Esta es una herramienta muy útil para localizar errores más adelante en la operación.

Así que si tengo un acceso a la base de datos, escribo una salida de registro en la depuración de nivel de registro como el SQL asociado fue montado. Si este SQL conduce a una excepción porque contiene un error, esta excepción se escribe con el error de nivel de registro. Si, por el contrario, se realiza una simple consulta de búsqueda con una sintaxis SQL correcta y el conjunto de resultados está vacío, este evento se clasifica como Debug o Warning, en función de las necesidades. Por ejemplo, si se trata de una solicitud de inicio de sesión con un nombre de usuario o una contraseña incorrectos, suelo optar por el nivel de registro Advertencia, ya que puede contener aspectos relacionados con la seguridad durante el funcionamiento.

En el contexto general, tiendo a configurar el registro para la ejecución del caso de prueba de forma muy locuaz y me limito a una salida de consola pura. Durante el funcionamiento, la información de registro se escribe en un archivo de registro.

El huevo o la gallina

Una vez que hemos sentado las bases para un bucle de retroalimentación adicional con el registro, el siguiente paso es decidir qué hacer a continuación. Como ya he mencionado, me resulta muy difícil escribir primero un caso de prueba y luego encontrar una implementación adecuada para él. Muchos otros desarrolladores que empiezan con TDD también se enfrentan a este problema.

Una cosa que ya puedo anticipar es el problema de asegurarse de que una implementación es comprobable. Una vez que tengo el caso de prueba, inmediatamente me doy cuenta de si lo que estoy creando es realmente comprobable. Los desarrolladores TDD experimentados han aprendido rápidamente en carne y hueso cómo debe ser el código comprobable. El punto más importante aquí es que los métodos siempre deben tener un valor de retorno que preferiblemente no sea nulo. Esto se puede conseguir, por ejemplo, devolviendo una lista vacía en lugar de null.

El requisito de tener un valor de retorno se debe a la forma en que funcionan los marcos de pruebas unitarias. Un caso de prueba compara el valor de retorno de un método con un valor esperado. La aserción de prueba tiene diferentes características y por lo tanto puede ser: igual, desigual, verdadero o falso. Por supuesto, aquí también hay diferentes variaciones. Por ejemplo, es posible probar métodos que no tienen valor de retorno utilizando excepciones. Todos estos detalles se aclaran en muy poco tiempo durante el uso de TDD. De modo que todo el mundo puede empezar inmediatamente sin largas preparaciones.

Al leer el libro Test Driven Development by Example de Kent Beck, también encontramos rápidamente una explicación de por qué los casos de prueba deben escribirse primero. Se trata de un factor psicológico. Debería ayudarnos a sobrellevar mejor el estrés habitual que surge en el proyecto. Crea en nosotros un estado mental sobre el estado y el progreso del trabajo actual. Nos guía en un proceso iterativo para ampliar y mejorar paso a paso la solución existente a través de los distintos casos de prueba.

Para quienes, como yo, no tienen una idea concreta del resultado final al inicio de una aplicación, este enfoque es difícil de aplicar. El efecto previsto de relajación se convierte en negativo. Como todos los seres humanos somos diferentes, tenemos que averiguar qué es lo que nos hace funcionar para conseguir el mejor resultado posible. Lo mismo ocurre con las estrategias de aprendizaje. Algunas personas procesan mejor la información visualmente, otras de forma más háptica y otras extraen todo lo importante de las palabras habladas. Así que intentemos no doblegarnos contra nuestra naturaleza para producir resultados mediocres o pobres.

Trazar la primera línea

Un tema sólo se me aclara mientras trabajo en él. Así que pruebo a ponerlo en práctica hasta que necesito un primer feedback. Es entonces cuando escribo la primera prueba. Este enfoque da lugar automáticamente a preguntas, cada una de las cuales merece su propio caso de prueba. ¿Puedo encontrar todos los resultados disponibles? ¿Qué ocurre si el conjunto de resultados está vacío? ¿Cómo se puede reducir el conjunto de resultados? Todas estas cuestiones pueden anotarse en un papel y marcarse paso a paso. La idea de anotar en un papel una lista de tareas pendientes la tuve mucho antes de que apareciera en el libro de Kent Beck mencionado anteriormente. Me ayuda a conservar pensamientos rápidos sin distraerme de lo que estoy haciendo en ese momento. También me da una sensación de logro al final del día.

Dado que no espero hasta que he implementado todo para escribir la primera prueba, este enfoque también da lugar a un enfoque iterativo. También me doy cuenta muy rápidamente si mi diseño no es suficientemente comprobable, ya que recibo feedback inmediato. Esto da lugar a mi propia interpretación de TDD, que se caracteriza por el cambio permanente entre la implementación y la escritura de pruebas.

Como resultado de mis primeros intentos de TDD, ya noté una aceleración de mis métodos de trabajo en la primera semana. También adquirí más confianza. Pero mi forma de programar también empezó a cambiar muy pronto. He notado que mi código se ha vuelto más compacto y robusto. Cosas que sólo se habían hecho evidentes con el tiempo surgieron durante actividades como la refactorización y las ampliaciones. Los casos de prueba fallidos me han salvado de sorpresas desagradables.

Empezar sin exceso de celo

Si decidimos utilizar TDD en un proyecto existente, es una mala idea empezar a escribir casos de prueba para la funcionalidad existente. Aparte del tiempo que hay que planificar para ello, el resultado no cumplirá las altas expectativas.

Uno de los problemas es que ahora hay que familiarizarse con cada funcionalidad y esto lleva mucho tiempo. La calidad de los casos de prueba resultantes también es inadecuada. El problema también surge de la falta de experiencia. Cuando se empieza a acumular experiencia, la calidad de los casos de prueba tampoco es óptima y es posible que haya que reescribir el código para que se pueda probar. Esto crea muchos riesgos que son problemáticos para el día a día del proyecto.

Un procedimiento probado para introducir TDD es simplemente utilizarlo para la implementación actual en la que se está trabajando. El estado actual del problema actual se documenta mediante pruebas automatizadas. Como ya estás en territorio conocido, no tienes que familiarizarte con un tema nuevo, así que puedes concentrarte plenamente en formular pruebas significativas. Aparte del hecho de que asumes la responsabilidad del trabajo de otras personas sin que te lo pidan cuando implementas casos de prueba para ellos.

La funcionalidad existente sólo se complementa con casos de prueba cuando se corrigen errores. Para la corrección, hay que ocuparse de todos modos de los detalles de implementación, de modo que aquí se sabe lo suficiente sobre cómo debe comportarse una funcionalidad. Las pruebas resultantes también documentan la corrección y garantizan que el comportamiento no cambie en el futuro durante los trabajos de optimización.

Si sigue este procedimiento de forma disciplinada, no se perderá en la llamada actividad frenética, que a su vez es lo contrario de la productividad. Además, adquirirá rápidamente conocimientos sobre cómo implementar pruebas eficaces y significativas. Sólo cuando se haya adquirido suficiente experiencia y, posiblemente, se planifique una amplia refactorización, podrá plantearse cómo mejorar gradualmente la cobertura de las pruebas para todo el proyecto.

Nivel de calidad

Que haya casos de prueba disponibles no significa que sean significativos. Una cobertura de pruebas elevada tampoco demuestra que un programa no contenga errores. Una cobertura de pruebas alta sólo garantiza que un programa se comporta dentro del ámbito de las pruebas.

Entonces, ¿cómo asegurarse de que las pruebas existentes suponen realmente un enriquecimiento y tienen un buen valor informativo? El primer punto, y en mi opinión el más importante, es que los casos de prueba sean lo más breves posible. En concreto, esto significa que una prueba sólo responde a una pregunta explícita, por ejemplo: ¿Qué ocurre si el conjunto de resultados está vacío? El método de prueba se denomina en función de la pregunta. El valor añadido de este enfoque surge cuando el caso de prueba falla. Si la prueba es muy corta, a menudo es posible saber a partir del método de prueba cuál es el problema sin tener que dedicar mucho tiempo a familiarizarse con un caso de prueba.

Otro punto importante en el procedimiento TDD es comprobar la cobertura de la prueba para las líneas de código, así como para las ramas de mi funcionalidad implementada. Si, por ejemplo, no puedo simular la ocurrencia de una sola condición en una sentencia IF, esta condición se puede eliminar sin dudarlo.

Por supuesto, también tiene bastantes dependencias de bibliotecas externas en su propio proyecto. Ahora puede ocurrir que un método de esta librería lance una excepción que no pueda ser simulada por ningún caso de prueba. Esta es exactamente la razón por la que deberías esforzarte por conseguir una alta cobertura de pruebas, pero no desesperar si no se puede alcanzar el 100%. Especialmente cuando se introduce TDD, una buena medida de cobertura de pruebas superior al 85% es habitual. A medida que el equipo de desarrollo adquiere experiencia, este valor puede incrementarse hasta el 95%.

Por último, sin embargo, hay que tener en cuenta que no hay que dejarse llevar demasiado. Porque puede convertirse rápidamente en excesivo y entonces todas las ventajas obtenidas se pierden rápidamente. La cuestión es que no escribas pruebas que a su vez prueben pruebas. Aquí es donde el gato se muerde la cola. Esto también se aplica a las bibliotecas de terceros. Tampoco se escriben pruebas para ellas. Kent Beck es muy claro al respecto: “Aunque haya buenas razones para desconfiar del código ajeno, no lo pruebes. El código externo requiere más de tu propia lógica de implementación”.

Lecciones aprendidas

Las lecciones que se pueden aprender cuando se intenta conseguir la mayor cobertura de pruebas posible son las que repercutirán en la programación futura. El código se vuelve más compacto y robusto.

La productividad aumenta simplemente porque se evita el trabajo monótono y propenso a errores gracias a la automatización. No hay pasos de trabajo adicionales porque los viejos hábitos se sustituyen por otros nuevos y mejores.

Un efecto que he observado una y otra vez es que cuando miembros individuales del equipo han optado por TDD, sus éxitos se reconocen rápidamente. En pocas semanas, todo el equipo había desarrollado el TDD. Cada uno según sus capacidades. Algunos con Test First, otros como acabo de describir. Al final, lo que cuenta es el resultado y era uniformemente excelente. Cuando el trabajo es más fácil y al final del día cada individuo tiene la sensación de que también ha conseguido algo, esto da al equipo un enorme impulso de motivación, lo que da al proyecto y al ambiente de trabajo un enorme impulso. ¿A qué espera? Pruébelo usted mismo ahora mismo.

No post found

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *