2.4 ¡A cubierto!

2.4 ¡A cubierto!

Capítulo publicado el 24/3/2022 por Enric Caumons Gou (@caumons)
  • 8 min de lectura

Tener una buena suite de test es muy importante, especialmente si se trabaja en proyectos grandes, complejos o con múltiples desarrolladores, así como en sistemas importantes y/o con mucha carga de trabajo. Aunque, recuerda que lo ideal es disponer de test en cualquier proyecto.

La cobertura de código es el porcentaje que mide el grado en que el código fuente de un programa ha sido comprobado mediante test. Por lo tanto, con una buena cobertura tenemos mayores garantías de éxito a la hora de desplegar nuevos cambios, aunque esto no significa que tengamos la certeza absoluta de que no va a fallar nada. Es decir, tener una cobertura de código del 100 % no significa que el sistema sea 100 % seguro, sino que los test que se han definido ejecutan toda la base de código, lo cual a priori es lo deseable, pero está claro que los test tienen que hacer pruebas reales y con sentido.

Además, hay que tener en cuenta que conseguir una gran cobertura y mantener las pruebas actualizadas, a medida que la base de código va creciendo y evolucionando, puede suponer un sobrecoste y un sobreesfuerzo considerables. Por este motivo, en ocasiones se acaba optando por soluciones de compromiso entre el grado de cobertura deseado y la cantidad de horas disponibles para el desarrollo y el mantenimiento de los test.

Existen muchos tipos de test (o pruebas), por ejemplo:

  • Pruebas unitarias: validan las unidades de código por separado, por ejemplo, las funciones o las clases. Son las pruebas de más bajo nivel.
  • Pruebas funcionalesvalidan que la aplicación funcione correctamente desde el punto de vista del usuario, sin conocer las partes internas del sistema. En el mundo web se usa mucho Selenium para automatizar este tipo de pruebas.
  • Pruebas de carga: validan la respuesta de la aplicación ante un determinado número de peticiones. De esta forma se puede detectar a partir de qué número de usuarios concurrentes hay riesgo de que la aplicación empiece a degradarse.
  • Pruebas de estrés: a diferencia de las pruebas de carga, estas se usan para ver en qué punto la aplicación falla (muy diferente de empezar a degradarse) debido al exceso de carga. Por lo tanto, permiten saber cuál es la capacidad de carga máxima que podrá soportar.

Por supuesto, hay que hacer las pruebas de rendimiento de software (carga, estrés, etc.) usando entornos lo más parecidos posibles a los reales y con volúmenes de datos similares. Es decir, no vale hacer las pruebas con las tablas de la base de datos vacías si el sistema real contiene miles o incluso millones de registros. Por otro lado, también es un autoengaño hacer las pruebas en sistemas más potentes que los que se van a usar realmente. Además, me gustaría añadir que es un grave error dejar este tipo de pruebas para el final, justo antes del día de la entrega (como si se tratara de un mero trámite burocrático), con la excusa de que el resto de pruebas son satisfactorias. Quizás funcione todo bien con un usuario, pero el mero hecho de que se conecten diez de forma concurrente puede hacer que veas los jinetes del apocalipsis acercándose porque la fecha de entrega está a la vuelta de la esquina, el sistema no aguanta y el fin del mundo se acerca. En realidad, ver que el sistema no va a aguantar la carga es otra de las causas frecuentes de insomnio. O sea que, si te gusta dormir tranquilo, no dejes los deberes para última hora.

Haces dos proyectos

Lo ideal es hacer los test paralelamente con el desarrollo del propio software, o incluso antes. De hecho, existe una metodología de desarrollo llamada Test Driven Development (TDD), donde, a partir de los test, se llega a la implementación del código real. Aun así, en muchos casos, los test siguen siendo la gran asignatura pendiente y se dejan para el final, si es que se acaban implementando. Desgraciadamente, en demasiadas ocasiones se siguen usando metodologías más parecidas a algo como Complaint Driven Development (CDD), es decir, desarrollo en base a quejas, ¿te imaginas por qué?

Sí, es cierto que disponer de una buena suite de test ralentiza el desarrollo del proyecto porque realmente tienes que hacer y mantener dos proyectos: el código en sí y los test. A veces, incluso, hay más código en los test que en las implementaciones de las funcionalidades a validar. No obstante, es conveniente disponer de ellos, al menos en los casos citados anteriormente. Ten en cuenta que la automatización de las pruebas (al menos las consideradas más importantes) debería ahorrarnos tiempo a medio o largo plazo, gracias a no tener que realizar las mismas comprobaciones manualmente de forma repetitiva y error-prone.

Así como en el desarrollo de software intentamos no repetir código, en el mundo del testing esta regla no siempre se cumple y a veces acabamos teniendo código un poco repetitivo para hacer las distintas pruebas. No obstante, no hace falta decir que hay que evitar los churros infumables de código espagueti. Escribir test no significa tener carta blanca para hacer locuras. Es más, el código de los test debe ser limpio, fácil de entender y de modificar. De no ser así, estos test serán abandonados progresivamente, hasta que llegue un momento en el que sean desactivados y se acabe trabajando sin ninguna cobertura de código.

Test recursivos

¿Quién vigila al vigilante? Los test tienen que ser sencillos porque, si hacemos virguerías y cometemos errores en los propios test, podemos falsear los resultados de las pruebas de forma fatídica, lo cual puede contribuir a una caída de pelo prematura. Unos test erróneos pueden ser un quebradero de cabeza realmente importante. Si implementáramos código complejo en los test, al final necesitaríamos más test para probar que los test que validan el código real son correctos. Esto no tiene sentido, ya que llevado al extremo llegaríamos a un bucle infinito de pruebas sobre pruebas y eso no lo queremos.

Dicho lo cual, es mejor escribir test sencillos y efectivos que hacer filigranas de las que no estemos seguros y que nos puedan dar resultados inesperados. Ten en cuenta que, en procedimientos de despliegue automatizado donde se usan herramientas de integración continua y de entrega continua (CI/CD), unos test que fallen pueden parar todo el flujo, con el consiguiente impacto negativo. Esto significa que no hay que tomárselo a la ligera y menos en sistemas donde los test se ejecutan en pipelines automatizadas.

Rehacer proyecto y test

Rehacer los test a medida que se hacen cambios en la base de código existente supone un esfuerzo extra para el equipo de desarrollo y, por este motivo, hay que tenerlo en cuenta a la hora de cuantificar las horas asignadas a las tareas. Una refactorización de código puede romper multitud de test en cadena, lo cual no es nada agradable y frena el desarrollo, ya que nos obliga a actualizar los test existentes y adaptarlos al nuevo código. No obstante, si los actualizamos, nos aseguraremos de que la nueva implementación no romperá las pruebas que se habían establecido previamente.

Es cierto que, a medida que los proyectos crecen, los cambios pueden ser mucho más costosos. Por ejemplo, si renombramos una función, tendremos que cambiar todas las llamadas que se hagan en el proyecto. Por suerte, disponemos de herramientas que nos pueden ayudar mucho en este tipo de tareas, pero hay que hacerlas y ya sabemos que cualquier cambio introduce un cierto nivel de riesgo. No hay que subestimar nunca ningún cambio, por pequeño y trivial que pueda parecer en un principio, porque siempre hay el riesgo de introducir un error «tonto» que puede hacer saltar por los aires el sistema entero. No sería la primera ni la última vez que ocurre…

Lo bueno de tener la suite de test actualizada y con una buena cobertura es que nos da una mayor seguridad de que los despliegues van a funcionar correctamente y nos podemos ahorrar muchos disgustos. Con esto conseguiremos aumentar la frecuencia de los despliegues en producción y acortaremos la duración de cada uno de ellos.

Falsear llamadas a recursos externos

Hay que ir con mucho cuidado con las llamadas a recursos externos durante la ejecución de test. Los sistemas no están aislados, sino que están conectados entre sí (la magia de Internet). El hecho de ejecutar pruebas automatizadas puede desencadenar llamadas no deseadas a otros sistemas y provocar cambios en sus respectivos entornos, incluyendo el de producción. Esto podría ser una verdadera catástrofe, por lo tanto, hay que diseñar los test con mucho cuidado y prever posibles efectos colaterales potencialmente mortales. Además de hacer modificaciones no deseadas, también puede hacernos exceder los límites máximos permitidos en determinados servicios, por ejemplo, debido al elevado número de llamadas realizadas a API externas o al número de correos electrónicos enviados. Pero esto no es todo, ya que también puede significar un incremento de costes de forma directa. Por ejemplo, supongamos que cuando se realizan determinadas operaciones se envían mensajes SMS y las pruebas automatizadas provocan un envío masivo. Si a las molestias ocasionadas a los receptores de los mensajes le añadimos que cada uno de ellos implica un cierto coste económico, imagina el posible resultado…

Entonces la pregunta es: ¿cómo diablos hacemos pruebas sin liarla parda con otros sistemas? Una posible solución es usar dobles de test como los mocks o stubs, entre otros. Es decir, usamos objetos simulados con fines de prueba, en lugar de objetos reales. Supongamos que un método tiene que hacer una llamada a un sistema remoto y devolver un fichero en formato JSON. Para falsear esta llamada, podemos crear un doble de test para que devuelva directamente un fichero JSON con el contenido que nos interese y así podamos validar nuestro sistema, sin que se haga ninguna petición a ningún sistema remoto. Podrás pensar que esto no es 100 % fiel a la realidad, es cierto, pero ya he dicho que una cobertura de código del 100 % no garantiza al 100 % que el sistema esté completamente libre de bugs. Además, conseguir una cobertura total puede ser realmente complejo y costoso debido a la dificultad que conlleva automatizar ciertas pruebas.

No seas malo

Hay organizaciones en las que se exige una cobertura de código mínima para poder desplegar cambios, si no, no se pueden realizar. Imaginemos ahora que tenemos un programa con 100 líneas de código. Si no hay definido ningún test, entonces la cobertura es del 0 %, en cambio, si creamos una suite de test que ejecute 50 líneas de este código, entonces tendremos una cobertura del 50 %.

Si fuéramos «malas personas», podríamos crear una función «tonta» de 100 líneas de código que lo único que hiciera fuera incrementar una variable que no se use para nada (un incremento por línea). Entonces, si hacemos un solo test que llame a esta función habremos ejecutado 100 líneas de código (absurdo) y también tendremos una (falsa) cobertura de código del 50 %. Si estábamos obligados a una cobertura mínima del 50 %, entonces ya podríamos desplegar los cambios sin tener ningún test real y nos acabaríamos de saltar el proceso de validación. Por supuesto, a medida que aumente la base de código, tendremos que alargar más esta función para que se siga ejecutando el 50 % del código.

Lo que acabo de explicar es un caso real que me contaron unos compañeros hace algún tiempo, y ocurrió cuando un equipo no había implementado test y querían subir cambios a producción. Lo que hicieron tiene una doble lectura, por un lado, son unos cracks del pensamiento lateral, pero, por el otro, no tienen perdón. Sin embargo, también podrían haber optado por otra opción menos engorrosa, implementando test falsos en los que se ejecutara código de la aplicación, pero no se hiciera ninguna verificación real.

De todos modos, quiero dejar claro que esto es solo un ejemplo de lo que no se debe hacer. No obstante, hay que tener en cuenta que este tipo de actuaciones para salirse por la tangente serán más frecuentes y propensas cuando se exija mucho, pero se ofrezca poco a cambio. Tal y como dice la Ley de Lister: «La gente bajo presión no piensa más rápido».

¡Únete a la comunidad para no perderte nada!

¡Quiero unirme!

¿Qué te ha parecido este capítulo?

¡Compártelo!

¡Suscríbete al feed para estar al día cada vez que se publique un nuevo capítulo!

Comprar libro

${ commentsData.total }

Todavía no hay comentarios. ¡Sé el primero!

Inicia sesión para publicar, responder o reaccionar a los comentarios.

Esta web utiliza cookies. Si continúas usándola, asumiremos que estás de acuerdo.