Este es un documento “vivo”. En él cual trato de plasmar mi pensamiento actual sobre pruebas automatizadas, de igual forma que mantener un registro conforme aprendo nuevas ideas y cambio de opinión.
La mentalidad sobre las pruebas automatizadas
No soy capaz de recordar una sola mención sobre pruebas automatizadas durante mi paso por la universidad. No fue, sino la búsqueda de información sobre “buenas prácticas” lo que me llevo a conocer sobre este tema.
A pesar de leer sobre sus beneficios y creer en ellos, no era capaz de materializarlo en código. Tenía una desconexión entre la teoría y la practica. Además de una carencia fundamental de entendimiento, acepte lo que otros decían sobre los beneficios del testing sin realmente comprender e interiorizar dichos beneficios, era una creencia mecánica.
Durante mis primeras experiencias profesionales trabajé en proyectos y empresas donde escribir pruebas no era parte del trabajo, por lo que ningún miembro del equipo escribió una sola prueba. Incluyéndome por supuesto. Introducir el mismo error múltiples veces era algo común.
Eventualmente, llegué a una compañía donde era necesario escribir pruebas. Si bien “creía” en los beneficios de escribir pruebas, era una simple repetición de algún argumento que leí en algún lugar. Realmente no entendía por qué eran importantes. No había experimentado sus beneficios.
Así que, las primeras semanas o quizás meses, escribir pruebas se sentía como hacer el trabajo dos veces. Ya terminé mi funcionalidad y “funciona” en mi computadora. ¿Por qué debería escribir más código para algo que ya sé que funciona?
Hasta que un día, realizando un cambio aparentemente sencillo, y a modo de simple comprobación, ejecute la suite de pruebas con total convencimiento de que nada cambiaria. Había realizado un trabajo perfecto.
Pero una de las pruebas dijo lo contrario. Una prueba estaba fallando, en una funcionalidad que ni siquiera estaba modificando. ¿Cómo es posible que una prueba de una funcionalidad “no relacionada” este fallando?
Una rápida inspección de la prueba fallida, y del módulo que estaba probando me dio la respuesta. Había codigo que se reutilizaba en ambos módulos y yo había cambiado su comportamiento.
Me tomo menos de un minuto hacer el cambio necesario para que funcionara en ambos lugares. Las pruebas automatizadas me salvaron de introducir un error de manera inadvertida, en un cambio que yo consideraba trivial.
Fue en ese momento, que mi mentalidad cambio. Y de pronto escribir pruebas, nunca más se sintió como trabajar dos veces, se volvió parte natural del ciclo de desarrollo. Escribir una nueva funcionalidad y las respectivas pruebas ahora son partes de la misma actividad.
Sin mencionar que el tiempo que me toma desarrollar algo nuevo no cambio, a pesar de que la intuición nos lleva a pensar que al implicar trabajo extra, implica, por lo tanto, tiempo extra. Sin embargo, al menos en mi experiencia, este trabajo extra se ve compensado por el hecho de que mantener una batería de pruebas saludable ayuda a mantener la velocidad de desarrollo conforme el proyecto se vuelve más grande, los requerimientos cambian y se introducen nuevas funcionalidades.
Aunque es cierto que en proyectos legacy, tratar de incluir pruebas es mucho mas dificil debido a la complejidad y deuda tecnica acumulada. Suele dar mejores resultados utilizar tecnicas para trabajar con codigo legado y agregar pruebas al codigo nuevo, desplazando poco a poco el codigo anterior mas dificil de probar.
Pirámide de pruebas
La pirámide de pruebas1 es un concepto muy debatido. Habla sobre los distintos niveles de pruebas que podemos incluir, desde las pruebas unitarias hasta las pruebas e2e (end to end). Con un número increíble de tipos de pruebas entre estos dos niveles.
Uno de los puntos de debate es que tantas pruebas de cada tipo necesitamos, al mismo tiempo que buscamos mantener una batería de pruebas rápida y confiable.
Mientras que las pruebas unitarias son rápidas de escribir y ejecutar, no suelen proveer retroalimentación de como el usuario usa nuestro software. Aunque claro, se puede hablar horas sobre que tan unitario es lo unitario2.
En cambio, las pruebas e2e, si bien prueban flujos completos tal cual lo haría un usuario, son muy caras de escribir y ejecutar. Sobre todo en los tiempos de Microservicios o aplicaciones separadas para el backed y el frontend. Al grado que algunas empresas han decidido abandonar este tipo de pruebas. 3
Lo importante, como menciona Kent Beck 4 es escribir el menor número de pruebas necesarias, para alcanzar el nivel de confianza requerido en nuestro proyecto.
La confianza de que al realizar un cambio y mis pruebas estén en verde todo se sigue comportando como se espera o, por el contrario, si alguna prueba está en rojo, algo cambio.
Esta confianza, ademas, nos ayuda a integrar cambios más rápido. Lo que a su vez nos permite reducir el riesgo de cada cambio y cada nueva funcionalidad introducida.
Nos da la confianza para entregar software lo más rápido posible.
En general encuentro que las pruebas de integración y las pruebas unitarias sociables hacen un gran trabajo brindando esa confianza que buscamos. Por lo que suelo escribirlas más, incluyendo aquellas que atacan a una base de datos de pruebas. Prefiriendolas sobre pruebas unitarias solitarias que aíslan completamente los colaboradores y otros subsistemas como la base de datos.
Si bien atacar directamente a una base de datos real puede ralentizar la ejecución de nuestras pruebas. Hoy en día bases de datos como Postgres soportan la ejecución concurrente de pruebas 5 y hay servidores de CI sofisticados capaces de paralelizar la ejecución de una masiva bateria de pruenas6. De otra forma, no podríamos verificar el esquema o las restricciones en nuestra base de datos.
Sin embargo, esto puede enfrentarse a sus límites (aunque cada vez mas lejanos) en sistemas muy grandes y complejos. Hay proyectos que pueden tomar horas en correr en el pipeline de CI por la gran cantidad de pruebas que tienen.
Un caso especial son los sistemas de terceros, como una API REST externa. En cuyo caso suelo recurrir al uso de mocks y stubs.
Mocks
WIP
Refactorización
WIP
Confianza
WIP
TDD
Test Driven Development es de esas cosas de las que se lee mucho, pero se ve poco en la practica, o al menos en mi experiencia personal. Seguramente hay empresas y lugares donde es una practica comun, pero no he podido verlo de primera mano.
Al inicio, escribir una prueba antes de saber si quiera como se va a llamar nuestra clase o modulo puede parecer carente de sentido. Aun no sabemos como va a lucir ese código, como podria comenzar escribiendo una prueba? Es algo contraintuitivo.
Pero es precisamente este ejercicio, el que nos empuja a pensar en el diseño del codigo y su interfaz, por adelantado en lugar de hacerlo reaccionando a nuestro intento inicial.
Eh encontrado que es mas facil iniciar con TDD al resolver un bug. Cuando se reporta un bug, primero se escribe una prueba que reproduzca el error, entonces debugueas y corrijes el error. Si la prueba pasa efectivamente se corrijio el error. Ademas esto garantiza que no se vuelva a introducir el mismo error en el futuro.
De esta forma es mas sencillo adoptar el pensamiento de escribir primero la prueba. Un cambio de pensamiento que es incomodo al principio, porque implica dejar de hacer las cosas de la forma en la que estamos acostumbrados.
Lo relaciono al tipo de incomodidad que se experimenta la primera vez que trabajas con un lenguaje funcional si vienes de lenguajes orientados a objetos. Cuando comienzas a experimentar caracteristicas como la inmutabilidad, es extraño, no es “intuitivo”. Eso no significa que sea malo, o mas dificil. Pero es diferente y hay que acostumbrarse y adapar nuestra forma de pensar.
Cuando comenzamos a hacer algo nuevo siempre es dificil al principio. Solo se supera practicando. Como dicen: La frecuencia reduce la dificultad 7.
Otro uso util de TDD y las pruebas en general es que ayudan al diseño del código. Al menos de manera indirecta. No es que te digan como diseñar tu código, pero escribir una prueba hace emerger los smell codes del diseño en forma de un setup complejo, o dificultad para probar el comportamiento buscado.
Si una prueba es dificil de escribir, significa que el código es dificil de probar. Si es dificil de probar probablemente es porque hace demasiadas cosas o tiene demasiadas dependencias o alguno de los tantos code smells existentes. Es decir, podemos mejorar el diseño de ese código.
Como con cualquier práctica hay promotores y detractores. Aunque soy un promotor, lo trato como cualquier regla o principio:
“Every rule has its exceptions, and every principle has its limits”
Esta frase del libro A Philosophy of Software Design siempre me recuerda que hay que desarrollar criterio para aplicar cualquier herramienta o pieza de conocimiento. Tanto para tener cautela, como para desafiar el “estatus quo”.
Integración continua
WIP