Artículo de invitado de Sergio Martínez, desarrollador Android senior en Gigigo y en Applivery. Amante de las cosas bien hechas y de no irse a la cama sin aprender algo nuevo.

El testing es una técnica de validación de nuestro código que, aunque lleva existiendo desde hace mucho tiempo, no ha sido hasta hace pocos años que ha empezado a cobrar la importancia que se merece. Todo desarrollador de calidad debe conocer los conceptos principales sobre el testing y ser capaz de utilizarlos en su día a día. Así que si nunca habías oído hablar del testing, o bien te suena el concepto pero nunca has adentrado en él, te recomiendo que no te pierdas esta serie de artículos.

En el artículo de hoy vamos a empezar hablando sobre testing en general. Sobre por qué hacer tests al software que desarrollamos, sobre sus ventajas, vamos a destapar mitos y falsas creencias acerca del testing. Después veremos los tipos de test que hay y en que consiste cada tipo. De todos esos tipos nos vamos a centrar en el más importante, aunque es cierto que todos son importantes… Vamos a centrarnos en hablar sobre Test unitarios, ¿por qué catalogamos a los unitarios como los más importantes? ¡¡sigue leyendo y lo descubrirás!!

¿Por qué la necesidad de hacer test?

  • Porque nos ayudan a comprobar que lo que desarrollamos hace lo que tiene que hacer: evitamos comportamientos inesperados y excepciones.
  • Nos permiten desarrollar las funcionalidades de una manera completa sin atarnos al tiempo que requiere ejecutar el software completo. Por lo tanto el tiempo de testing no es una pérdida de tiempo, al final es tiempo que ganamos, por este y por más motivos.
  • Nos obliga a que las responsabilidades se distribuyan de una manera uniforme y más correcta a lo largo de nuestro código.
  • Obtenemos como resultado código que sólo por haberlo desarrollado de un forma testable cumple muchos de los principios SOLID. Además este código desarrollado de esta forma nos va a ayudar en muchas ocasiones a detectar code smells.
  • Más facilidad a la hora de acometer cambios que no requieren que la funcionalidad cambie, o lo que es lo mismo podremos hacer refactor de nuestro código sin miedo a que se vea alterado el funcionamiento del resto del sistema.

SOLID y el testing

Como ya se ha hablado bastante de SOLID en la serie de artículos publicados con anterioridad, no voy a ponerme pesado con este tema, porque ya es de todos los seguidores conocido que aplicar estos principios nos aporta infinidad de ventajas. Si alguien no ha visto los 5 artículos de SOLID, está aún a tiempo de verlos antes de seguir leyendo por aquí.

Nos vamos a centrar en remarcar desde el punto de vista del testing lo que nos va a aportar respetar cada principio.

Principio de responsabilidad única: Para los test es importante que las responsabilidades estén segregadas y las clases y métodos hagan el menor trabajo posible. Esto es así porque cuando algún punto de nuestro código hace muchas labores esto quiere decir que los resultados pueden ser varios. Esto se traduce a testing en muchos casos posibles de salida que hay que testar.

Recuerda: Cuando hay diferentes entradas y puede haber diferentes caminos o acciones. El número de combinaciones de estados y comportamientos al final de una ejecución crece de manera exponencial.

Por eso es importante que nuestras clases y métodos respeten SRP.

El principio de abierto cerrado: si nuestro código crece a medida que evolucionamos nuestro software, es posible que no estemos distribuyendo bien las responsabilidades y que no estemos encapsulando lo que varía, por lo tanto vamos a tener que modificar nuestros tests, porque nuestro código testado está cambiando.

Recuerda: Cambiar en numerosas ocasiones un test cuando nuestro software crece por extensión y no por modificación directa de requerimientos es un claro indicador de la violación del principio abierto/cerrado.

Principio de sustitución de Liskov: como se ha comentado en el post de este principio, es muy fácil comprobar que se está violando cuando hacemos tests puesto que cuando hacemos test sobre la clase padre que no funcionan sobre la clase hija vemos una clara violación del mismo. Esto nos va a obligar a realizar nuevos test sobre algo que en principio estaba testado, lo cual no suena muy bien y empieza a no ser mantenible.

Recuerda: a la hora de usar la herencia, y sobre todo la sobreescritura de métodos de los supertipos ten en cuenta que tienes que pasar test sobre eso que vas a heredar y sobreescribir y piensa dos veces si es la mejor manera de hacerlo, o simplemente es mejor que hagas uso de una composición.

Principio de segregación de interfaces: si tenemos métodos que no sirven para nada en clases que sólo están ahí como consecuencia de implementar una interfaz de la cual nos interesa un sólo método, está claro el primer problema, como ya se ha visto eso genera un boilerplate indeseable… Pero a efectos de test, es incluso peor ¿Vamos a testar esos métodos boilerplate que no hacen nada?, ¡¡que cosa más fea!!, o testamos algo que no vale para nada o hacemos que baje nuestra cobertura de código testado por culpa de un mal diseño…

Recuerda: cada método de una interfaz es siempre un claro candidato a ser testado. Por lo tanto es bueno que nuestras interfaces sean concretas y específicas. De lo contrario tendremos varias implementaciones innecesarias que tendrán test inservibles.

Principio de Inversión de dependencias: sin duda este principio va a ser nuestro amigo fiel en la implementación de nuestros test. Es el que nos va a permitir adaptar el test a nuestras necesidades y recrear exactamente los escenarios que necesitemos para testar exclusivamente lo que queremos testar. Gracias a este principio nosotros establecemos el alcance o “Scope” de nuestro test. Se testará tanto código real como nosotros decidamos.

Recuerda: que nuestras implementaciones concretas no dependan del sujeto bajo pruebas sino de una abstracción de mayor nivel nos abre la puerta en tiempo de test a servir implementaciones exclusivas para el contexto de testing que nos permitan ejecutar sólo el código real que queremos testar.

Tipos de Test

Test de integración

Los test de integración nos ayudan a automatizar procesos en los cuales intervienen otros sistemas para detectar fallos o casos no esperados en la interfaz de comunicación. Testan como se integra o interacciones de nuestro sistema con un agente externo. Entre sus principales características encontramos:

  • No son especialmente rápidos a la hora de ejecutarse.
  • Requieren una preparación específica para crear un contexto de prueba que replique las condiciones que deseamos. Ej: podemos ejecutar un test de integración contra la BD que soporta Mongo y usa una caché externa y querer hacer un “Test-double” dé la pieza de Mongo para forzar a trabajar a la caché con unos resultados concretos.

Test de aceptación

Estos test también se suelen llamar de caja negra, puesto que validan resultados a acciones directas del usuario acorde a la descripción del negocio tratando el sistema como una caja negra.

Validan los requisitos del negocio, desde el punto de vista del usuario final. Tanto el resultado de las acciones como la apariencia final de la app. Pueden englobarse aquí los test de instrumentación, UI, pixel perfect, y alguno más.

Aunque el caso concreto de lo que se quiera testar sea muy reducido, de manera indirecta el test está pasando por gran parte de nuestro código, y de hecho está desarrollando una prueba end to end pero lo que pasa entre la acción y el resultado es una caja negra.

Test unitarios

Aquí viene la fiesta…

Son test también llamados de caja blanca. Se centran en probar que nuestro código clase por clase y método por método hace lo que tiene que hacer. Comprobando que el comportamiento y el estado del sistema sean los esperados. Es decir, a unas entradas, tras una ejecución esperamos unas salidas o que se hayan ejecutado otras “N” cosas.

Al principio del post catalogaba a los test unitarios como los más importantes y resulta que todavía no he dado un por qué, quizá muchos ya lo intuyen… La explicación es sencilla de contar pero llevarla a cabo no es tan sencillo y es algo que debemos ir consiguiendo de manera progresiva. Si nuestro código está desacoplado del framework y de los agentes externos al máximo y la dependencia entre estos y nuestro código es la mínima, en otras palabras, si somos capaces de aislar nuestro software; simplemente con la cobertura que nos brindan los test unitarios cubriremos la mayor parte de nuestro código. Además los test unitarios son rápidos y por lo tanto los podemos correr muy a menudo (con cada commit por ejemplo) para comprobar si todo sigue estando correcto. Llegar a desarrollar el 100% para que esté desacoplado y sea testable es algo que se va adquiriendo con la práctica, no te desesperes, la práctica hace la perfección.

Como podemos ver los test unitarios nos aportan infinidad de ventajas y son un punto ideal para introducirse en el mundo del testing. Con ellos podemos ir desde los conceptos más básicos hasta desarrollar pipelines de testing complejas, versátiles y muy potentes.

Es por eso que en esta serie de post vamos a ir desgranando los test unitarios desde lo más básico hasta conceptos algo más complejos, pasando por algún consejo para no caer en fallos comunes. Así que si quieres aprender de que van las “palabrejas” que ya hemos ido usando por aquí como “Test-double”, “Scope” y muchas cosas más, no dejes de seguir a devexperto.com/lista-de-correo.

En el siguiente artículo verás qué hace que un test sea un test. ¡Nos vemos pronto!