No pierdas el tiempo escribiendo tests

Hace unos días me dieron la oportunidad de hablar sobre testing en el grupo de CrossDevelopment Madrid y algunos me han pedido que compartiera la presentación que utilicé. Aunque podéis descargarla, lo cierto es que sin explicaciones acompañándola sirve de poco. Este post pretende ser una pequeña guía de lo que conté allí. No va a ser tan extenso como la presentación porque sería aburridísimo, pero dado que la mayoría de lo que comenté ya lo había escrito antes en el blog, servirá para repasar algunas ideas sobre testing y enlazaré posts que profundizan en esas ideas (en ocasiones, bastante más de lo que permitía el tiempo de la charla). Aun así, va a ser un post algo más largo de lo habitual.

Las dificultades iniciales

Empecé a utilizar tests automatizados hace mucho, sobre el año 2003 más o menos. En aquella época descubrí NUnit y decidí que sería buena idea evitarme probar a mano las aplicaciones, así que me puse a escribir tests sobre la aplicación que estaba desarrollando.

Era la típica aplicación de la época: N capas, servicios web SOAP, modelo anémico, capa de lógica de negocio que no hacía nada… Os podéis imaginar el resultado. La única forma de testear algo era atacando la base de datos, y para eso tenía que lanzar a mano un script para crearla, otro para preparla… Un desastre. Ejecuté muy pocas veces esos tests y no me sirvieron de nada.

De ahí pasé a comprender el concepto de test unitario. La clave era testear código independiente de factores externos. Y empecé a escribir tests unitarios. Sólo había un problema: casi no tenía código que no dependiera de algo complicado (una base de datos, un socket, un servidor, etc.). Así que acabé testeando mi modelo anémico. Es decir, mis tests básicamente se limitaban a comprobar que era capaz de asignar propiedades y luego leerlas. Muy poco útil.

Por suerte tropecé con nuevos conceptos: inversión de dependencias, inversión de control y mocks. Muchos mocks. Bueno, y fakes, y stubs, y spies y demás amigos. Ahora ya podía testear todo lo que quisiera, sólo necesitaba ocultar las depedencias detrás de interfaces y mockearlas.

Era una época en la que había aprendido de mis errores iniciales y había montado un servidor de integración continua para lanzar los tests con frecuencia e incluso todavía me creía las métricas, por lo que esto de los mocks era algo maravilloso que me permitía tener unos informes estupendos en los que la cobertura rozaba el 100%.

¿El problema? Que mis tests no me decían gran cosa. Los tests de interacción era frágiles, difíciles de leer, complicados de mantener y, cuando fallaban, muchas veces no estaba seguro de si lo que estaba mal era el test o el código de producción. De hecho, desde hace mucho tiempo trato de evitar utilizar mocks todo lo que puedo.

Todo esto puede llevarte a sentirte un poco engañado con esto de testing. Al final o testeas cosas inútiles (getters y setters) o tienes tests frágiles con mocks que no hay quién entienda. Puede parecer un poco exagerado, pero si dais una vuelta por GitHub seguro que encontráis alguna aplicación de esas que se supone que están bien hechas donde muchos tests caen en uno de esos dos casos.

Bueno, la realidad es un poco más complicada y mi problema real es que no tenía ni idea de cómo escribir buenos tests.

Para qué escribimos tests

Para poder escribir buenos tests, lo primero que tenemos que entender es para qué queremos tests, y para eso siempre me gusta recurrir a esta cita de Kent Beck (padre de TDD):

A mi me pagan por escribir código que funcione, no por escribir tests.

Nunca podemos perder esto de vista. Los tests son un medio para ayudarnos a conseguir mejorar la calidad de una aplicación, no un fin en si mismo. Escribir tests que no te aportan nada, no tiene ningún sentido. En mi caso, lo fundamental es que un test me aporte confianza en que el código de producción funciona.

Para ello, necesito:

  • Que valide algo útil. Si el test va a limitarse a validar que los getters y setters funcionan, probablemente mi confianza global en el código de producción no aumente mucho. Me interesa testear las partes de mi aplicación más complicadas, las que más lógica tienen, las que son más propensas a fallos, las que son más importantes para mi aplicación.
  • Que sea comprensible. Si el test es complicado de entender, es díficil saber si realmente testea lo que quiero, y además en caso de fallo tampoco sé si está mal el código de producción o el código de tests (y no, TDD no te salva de esto).
  • Que se ejecuten con frecuencia. No vale de nada tener una suite de 8000 tests, si nunca la ejecutas. Hay que buscar una forma de que se ejecuten con cierta periodicidad (idealmente en cada commit).

Además, siempre hay que recordar que nada es gratis y los tests tampoco lo son, por lo que también intento que mis tests sean fáciles de mantener y extender.

Con el tiempo, el código de producción cambia y si hacer cambios en él te supone tener que arreglar cientos de tests que no están relacionados directamente con ese cambio al final en lugar de ayudar se convierten en un elemento de fricción que frena el desarrollo. Si introducir un nuevo parámetro en un constructor hace que tenga que tocar 420 usos de ese constructor en los tests, a lo mejor me lleva a no introducir el parámetro, o hacerlo opcional, o sacrificar el diseño que quiero conseguir en aras de minimizar el mantenimiento de los tests.

Cómo escribir mejores tests

Seguro que existen muchas formas de conseguir cumplir con los puntos anteriores, pero la mejor forma que he encontrado hasta ahora es analizar la forma en que se relaciona el código de producción con los tests.

Al escribir tests encontramos distintos tipos de código a testear. A veces tenemos funciones puras, que son muy cómodas de testear porque sólo necesito pasarles parámetros, obtener resultados, y comprobar si son los esperados. En ocasiones tenemos código que depende del estado interno del objeto o módulo al que pertenece y por tanto el test es algo más complejo. Otra veces ni siquiera podemos observar el resultado del código ejecutado directamente, y tenemos que hacerlo a través de otros objetos. En el peor de los casos, nuestro código no tiene ningún estado observable y lo único que hace es generar efectos colaterales.

Merece la pena profundizar en los tipos de dependencias que podemos encontrar entre el código de producción y su estado observable para conocerlos mejor.

Recurriendo otra vez a citas pegadizas, esta vez de Edsger Dijstra:

La simplicidad es prerrequisito de la fiabilidad

Si lo que estamos buscando es poder fiarnos de nuestro código y nuestros tests, debemos intentar que nuestro código sea lo más simple posible y tratar de acercarnos a los primeros casos (funciones puras) y alejarnos de los últimos (interacciones no observables directamente). Conseguir esto es más sencillo de lo que parece, y muchas veces podemos encapsular gran parte de la lógica en un modelo sin demasiadas dependencias y refactorizar nuestro código para que tenga menos dependencias.

Si nos centramos en la parte de los tests propiamente dichos, debemos intentar desacoplarlos al máximo del código que no están testeando para evitar que se vean afectados por cambios en la aplicación que no tienen nada que ver con ellos. Si esto lo hacemos bien, ganaremos además legibilidad en nuestros tests porque tendrán menos código y quedará más claro qué partes son importantes para el test y qué partes no.

Existen muchas técnicas para ello. Por ejemplo, podemos evitar depender de otros constructores usando ObjectMothers. Cuando eso se nos queda corto y necesitamos tantas configuraciones de objetos que el ObjectMother ya no es práctico, podemos utilizar builders para independizarnos de APIs que no estamos testeando. Y si los builders nos parecen muy trabajosos de escribir, podemos recurrir a métodos de creación.

Todas estas alternativas ayudan a construir tests más legibles, en los que es más sencillo añadir nuevos casos de tests y que son más resistentes a cambios en código de producción que tiene poco que ver con ellos.

La pirámide de los tests

Todo esto está muy bien y nos puede ayudar a escribir tests unitarios que nos aporten más confianza, pero en una aplicación no todo se puede testear con tests unitarios. Normalmente se habla de la pirámide de los tests, que es una forma de categorizar los tests en base a su tipo:

test-pyramid

La idea es que en la base de la pirámide tendremos los tests unitarios, es decir, aquellos que son (teóricamente) fáciles de escribir y rápidos de ejecutar. Según vamos ascendiendo por la pirámide, encontramos tests que cuesta más escribir, tardan más en ejecutarse y comprueban partes más grandes del sistema. Se supone que siempre deberías tener muchos tests unitarios, menos de integración, y aún menos de extremo a extremo.

Desgraciadamente, la pirámide de los tests no siempre funciona.

Hay aplicaciones, que por el tipo de aplicación que son, no se prestan bien a ser testeadas con tests unitarios, aislados de todo, y necesitan otro tipo de tests. A veces la pirámide te puede llevar a usar diseños muy poco apropiados. Un ejempo claro es una aplicación de reporting contra una base de datos. Si te dejas guiar por la pirámide (y por las técnicas que hemos visto antes), es tentador acabar cargando mucha información en memoria para poder realizar las consultas allí y testearlas con tests unitarios. Lo malo es que cuando llegues a producción y tengas, en lugar de 10 registros, 10 millones, esa aproximación no es válida.

Hay que ser consciente del tipo de aplicación que estás desarrollando y no sacrificar características básicas, como un rendimiento razonable, en aras de mantener un dibujo muy bonito de una pirámide que has visto por ahí.

Al salir del mundo de los tests unitarios todo se complica un poco, pero poniendo algo de sentido común se pueden hacer cosas bastante razonables.

Existen algunos conceptos básicos que debemos tener en cuenta al testear una base de datos: es necesario ser capaz de crearla fácilmente y volver a un estado conocido entre test y test, y debemos prestar especial atención a la forma en que preparamos el setup del test porque suelen requerir más código que en los test unitarios, pero es factible escribir tests legibles contra una base de datos.

Testear un servidor web tampoco es una tarea imposible. Ni escribir tests contra el interfaz de usuario.

Obviamente, este tipo de tests son más costosos de desarrollar y de mantener, por lo que lo ideal es poder testear el máximo posible de la aplicación sin ellos (volvemos a la pirámide), pero eso no quiere decir que a veces no sean necesarios.

Otros usos de los tests

Una vez que tienes montada una infraestructura de testing, y has conseguido que tu servidor de integración continua los lance periódicamente, es un buen momento para intentar aprovecharla al máximo. Los tests no sólo son útiles para testear «lógica de negocio», sino que también los puedes utilizar para validar convenciones y evitar despistes.

Siempre hay partes del código que requieren trabajo repetitivo y que es fácil hacer mal, y los tests nos pueden ayudar a evitarlo. Por ejemplo, asignar la ruta a un controlador usando un atributo, o añadir un atributo Description a un valor enumerado para luego poder mostrar un texto más amigable en pantalla. A veces podemos cambiar el diseño para aprovechar el sistema de tipos y evitar este tipo de errores, pero si no es posible, se pueden escribir tests que validen nuestras convenciones. Para esto, los tests de aprobación resultan especialmente prácticos.

Con un poco de imaginación se pueden acabar escribiendo tests para validar muchas propiedades de nuestro código y garantizar que se siguen cumpliendo con el paso del tiempo.

Conclusiones

Al final, cualquier aplicación se va a testear y lo mejor que puedes hacer es elegir cómo. Intenta evitar que los tests los haga un cliente en producción y dormirás más tranquilo y serás más feliz.

Para ello, los tests automatizados son una herramienta muy útil (aunque no la única), siempre y cuando te sirvan para aumentar la confianza que tienes en que la aplicación funciona.

El coste de escribir tests no es despreciable (tampoco el de los errores en producción, ojo), así que trata de asegurarte de escribir tests que te aporten algo. Escribir tests sólo por quedar bien no vale para nada.

Ni todas las aplicaciones ni todos los equipos de desarrollo son iguales. Habrá casos en que compense utilizar un tipo de test frente a otro. Merece la pena conocer distintas técnicas de testing y trazar una estrategia de testing adecuada a cada caso, teniendo en cuenta el escenario que tienes delante.

7 comentarios en “No pierdas el tiempo escribiendo tests

  1. Hace unos cuantos años encontré un libro sobre tests enfocados a casos de diseño (Design Driven Testing: Test Smarter, Not Harder) que, la verdad, me gustó la idea en la que enfocaba sus ideas, pero que tuvo muy malas críticas porque ponía a parir el TDD y porque usaba una tecnología propietaria para currar llamada ICONIX que la verdad sobraba, pero las ideas que propone el libro me parecen muy buenas y leer tu post me recordó lo que leí en ese libro. Si puedes echarle un vistazo puede que interese porque propone patrones de trabajo con tests la mar de chulos.

  2. Vicente Fernández dijo:

    Hola Juan María.
    Llevo algo más de un año leyendo el blog y he de decir que me sirven de mucho tus publicaciones.
    Primeramente quería felicitarte por tu trabajo, me parece excepcional y sirve como referente a la gente que llevamos poco tiempo en este sector.
    Por otro lado una pena que no me enterase de la charla que diste, te animo a que sigas dando más charlas ya que seguro que hay mucha gente que te sigue y no se ha enterado como yo xD.
    Un saludo, Vicente.

Comentarios cerrados.