El día que conocí el valor de los unit tests
Intro
¿Cómo podemos probar esto localmente? - preguntó el desarrollador. No podemos - respondió el arquitecto.
Esa fue la frase que inició mi viaje hacia el mundo de las pruebas unitarias.
¿Cómo puedo garantizar un minimo nivel de calidad sobre algo que deifnitivamente no puedo probar, debido a dependencias externas y limitaciones de infraestructura? Y no siempre es cuestión de “arrojarlo a producción y ver que pasa”, ya que en muchos casos, el costo de un error puede ser muy alto; ciertos procesos involucran transacciones monetarias, o datos sensibles de y para los usuarios, y un error puede significar una pérdida de dinero, o una violación de una norma o una ley.
En mi caso, se trataba de dinero y de la reputación de un nuevo producto que estaba lanzando la compañía. Y el funcionamiento de nuestro programa dependía de sistemas externos, disponibles sólo en un entorno de producción. El costo de un error era muy alto.
No se trata de ser sofisticado, sino perezoso
Si bien lleva un tiempo familiarizarse con el frawework de pruebas, una vez dominado, este nos permitirá probar nuestro código de forma automatizada, tantas veces como queramos, y en un entorno controlado. No sólo seremos capaces de detectar errores de forma temprana, sino que además nos da la confianza necesaria para realizar cambios en el código sin temor a romperlo.
De hecho, escribir un test es en la mayoría de los casos la eleción más inteligente en términos de costo-beneficio.
Encapsulamiento y testabilidad
La gran ventaja que presentan los tests unitarios es su bajo costo:
- Son rápidos de escribir;
- Son rápidos de ejecutar;
- Son fáciles de mantener;
- No implican un gran consumo infraestructura (con excepción de los stress tests);
- No requieren de un entorno de pruebas complejo;
- Aseguran que lo que cambias en A, no rompa lo que funciona en B, C y D, sin necesidad de que sepas sobre la existencia de B, C y D.
- Pero por sobre todas las cosas, nos permiten encontrar fallas de diseño en nuestro código.
Estas fallas de diseño no son necesariamente faltas graves, pero en la enorme mayoría de los casos, son señales de que nuestro código no está bien encapsulado, lo cual a mediano plazo resulta en un código difícil de mantener, y por lo tanto, costoso y una pieza de basura que nadie se atreve a tocar, es decir, algo lleno de code smells. Y en base a esto, el siguiente punto.
“Este código no se puede probar”
La pregunta correcta es “cómo hemos llegado a este código?”. Bien, aquí hay otra gran razón para comenzar a utilizar unit tests de forma temprana. ¿Qué tan tempano? Lo antes posible, incluso antes de comenzar tu implementación si es que cuentas con un diseño mental que te lo permita.
Es que… no sé por dónde empezar
A todos nos ha pasado. Es la historia de la gallina o el huevo: cómo voy a probar algo que no existe? Y cómo voy a crear algo que no puedo probar? La respuesta es simple: comienza por lo más simple, y ve agregando complejidad a medida que avances.
Aunque hay un punto de partida: la arquitectura de tu sistema. Es ella la que te va a permitir identificar los puntos de entrada y salida de tu sistema, y por lo tanto, los puntos de entrada y salida de tus tests.
Intenta aislar la lógica de tu negocio
Escribas o no tests unitarios, siempre es saludable aislar la lógica de tu negocio en una capa separada, de forma que puedas probarla de forma aislada. Esto te permitirá probar tu código de forma manual, y te dará la confianza necesaria para refactorizarlo sin miedo a romperlo en caso de que necesites hacerlo.
Piensa siempre en escenarios
Un escenario es una situación que se presenta en tu sistema, y que puede debe ser probada. Por ejemplo, si tu sistema es un carrito de compras, un escenario podría ser agregar un producto al carrito, o eliminar un producto del carrito. Tu suite de tests debería ser pensada como una máquina que se acopla a ese componente que deseas probar, que le dará por cada prueba una entrada, y esperará una salida. Si te detienes a pensar, esto es igualmente aplicable a la vida real, cuando uno coloca in termómetro en la boca de un paciente, y espera una temperatura, o los más sofisticados bancos de prueba utilizados en la industria automotriz y mecánica en general. Para ello los frameworks de prueba suelen incluir la característica para asociar un proveedor de datos a un test case.
No olvides ningún caso extremo (edge case). Intenta pensar en todos los escenarios posibles, y escribe un test para cada uno de ellos. Cada posible camino que tome tu algoritmo debe ser probado; a esto se lo llama cobertura de código. Volviendo al ejemplo del carrito, un edge case podría ser agregar el mismo producto varias veces: ¿cómo debería comportarse el carrito en base a las expecificaciones de tu negocio? ¿Debería manejarlos como productos independientes, o dejar una única entrada e incrementar la cantidad?
Algunas otras preguntas generadas por chatGPT:
- ¿Qué pasa si el producto no está disponible?
- ¿Qué pasa si el producto no existe?
- ¿Qué pasa si el producto está en oferta?
- ¿Qué pasa si el producto está en oferta y además es un producto de temporada?