La carrera contra los tests

Grupo Esfera SA
Grupo Esfera Blog
Published in
8 min readMar 6, 2023

por Natalia Lehmann

“La carrera contra los tests siempre la estás perdiendo”. Esta frase de Alan Cyment me hizo reflexionar sobre el tiempo que invertimos en mantener las pruebas de aceptación, una práctica que por ser repetitiva, por la naturaleza frágil de las herramientas que usamos y por la demanda de tiempo que implica se abandona en muchos casos sin tomar conciencia del valor que aportan.
En Grupo Esfera venimos recorriendo hace años el camino de la automatización de pruebas y, si bien sabemos que se puede seguir mejorando mucho, tenemos algunos insights para compartir.

En primer lugar deberíamos hablar de la motivación: ¿Por qué hacemos pruebas automatizadas?
Para incrementar la calidad del código. No tanto para cumplir un estándar de calidad, sino para que sea más fácil incorporar funcionalidades nuevas. El software es un monstruo en permanente evolución, que si no crece se muere. Desde el desarrollo tenemos que cuidar y controlar ese crecimiento para que no se produzcan “ramificaciones” y que después no nos animemos a tocarlo porque se convirtió en algo espinoso.
Para que no nos de miedo tocar código es que tenemos una buena batería de tests que nos respalda. Entonces, hacemos tests para que sea más fácil cambiar el software.

Si, además, esas pruebas las escribimos antes de escribir el código (lo que proponen las prácticas de TDD y ATDD por ejemplo), esto nos aporta una serie de ventajas adicionales:

Nos obliga a definir expectativas: Primero pienso en que es lo que espero de este segmento de código o de esta funcionalidad, cuales son las pre-condiciones y qué espero obtener. Hago un pequeño test y una pequeña implementación, y avanzo en la definición una mejor y más completa implementación sólo en la medida en que pueda definir tests para cada resultado esperado.
Nos enfoca en lo que tiene prioridad para comenzar por ahí.
Nos ordena para pensar en todos los casos de prueba, no solo el camino feliz.
Nos guía a implementar en pequeños incrementos, lo cual es menos propenso a errores.
Disminuye la percepción de que los tests son una pérdida de tiempo, algo que suele pasar cuando los tests se definen a posteriori, cuando la implementación ya fue testeada manualmente. Hacerlo antes permite que cuando terminemos ya tengamos una buena cantidad de tests desarrollada.

Por otra parte, tener automatizadas las pruebas nos ofrece dos ventajas. En primer lugar, reducir el feedback loop, es decir, el tiempo que tardo en darme cuenta si el cambio que hice rompió funcionalidad anterior.
Hago un cambio, ejecuto todas las pruebas y, si todas pasaron, me aseguro que mi cambio no rompió otra funcionalidad. Esta ejecución de pruebas de regresión, ¿podría ser manual? Si, pero se tardaría mucho más tiempo. Además de darnos feedback casi instantáneamente, la automatización reduce el margen de error, haciendo repetible la prueba en cada oportunidad.

Pruebas de aceptación

En nuestro equipo de proyecto decidimos implementar pruebas de aceptación automatizadas desde el momento 0. Esta práctica hace que nos sentemos a la mesa todas las personas involucradas (analistas, desarrolladores, testers, usuario, cliente) a conversar sobre cada historia y transcribir eso en criterios de aceptación, lo cual se convierte en un documento ejecutable. Ese documento tendrá doble función: por un lado describir lo que hace mi sistema y a la vez verificar que se cumpla esa descripción.
Además, como mencionamos antes, cualquier conjunto de pruebas automatizadas nos sirve además como respaldo para hacer una regresión.

El problema de las pruebas de UI (user interface)

Ahora bien, como las pruebas de aceptación se construyen desde la perspectiva del usuario final, solemos caer en la (mala?) costumbre de describirlas desde la interfaz gráfica, implementándolas como pruebas de integración (E2E) que inician su recorrido desde la interacción del usuario con la pantalla.

Y este tipo de pruebas, las de UI, suelen ser muy poco eficientes:
Son lentas (porque requieren levantar un browser, construir el HTML completo de la pantalla, incluyendo tal vez imágenes y hojas de estilo que pueden requerir mucho tiempo de carga, encontrar objetos en el DOM, etc).
Son poco confiables, porque las herramientas que tenemos hoy en día no son muy estables a la hora de seleccionar partes de la pantalla o reproducir comportamientos. En otras palabras, dan falsos positivos y tienden a ser muy frágiles.

No nos ayudan a identificar un problema en caso de que se produzca. Cuando eso pasa, lo que vemos es el mensaje genérico que recibe el usuario pero no tenemos información detallada sobre el error que se produjo, en qué línea de código pasó o qué fue exactamente lo que falló.
Por todo lo anterior, suelen requerir mucho mantenimiento: invertimos muchísimo tiempo corrigiendo pruebas que fallan no por una implementación errónea sino por la naturaleza inestable de la prueba misma.

¿Conviene tener pruebas de interfaz gráfica automatizadas?

La respuesta corta es Sí pero, ¿en qué medida?

Aquí es donde podríamos recordar la recomendación de la Pirámide de Pruebas de Mike Cohn (1). Esta pirámide nos presenta una guía para organizar la cantidad de pruebas que deberíamos tener por cada tipo. Y lo que nos muestra es que deberíamos favorecer la implementación de pruebas unitarias por sobre todas las demás.

¿Por qué? Por varios motivos: son rápidas (se ejecutan en milésimas de segundo); son aisladas, es decir, no dependen unas de otras; son repetibles; no requieren de un chequeo manual sino que se validan a sí mismas. Son eficientes en el uso de un recurso siempre escaso: el tiempo.
Son sumamente útiles pero, por sí solas, no pueden testear la funcionalidad completa y no pueden garantizar que el sistema funcione bien en su totalidad.

Se hacen necesarios entonces otros tipos de pruebas para garantizar la corrección del sistema, necesitamos de distintos grados de pruebas de integración: desde la integración de algunos pocos componentes entre sí hasta la integración de punta a punta o E2E, incluyendo la interfaz gráfica.

¿Cómo logramos balancear la necesidad de contar con pruebas de integración con el deseo de reducir el tiempo que invertimos en mantenerlas vivas?

La respuesta a esta pregunta es sumamente compleja. No tenemos una regla de oro, sólo tenemos algunos consejos para ofrecer, que nos surgieron a partir del camino que recorrimos y que, seguramente, podrán ir modificándose y enriqueciéndose en el futuro.

Algunos consejos

  1. Preguntarnos ¿qué es lo que queremos testear en este caso? Si lo que realmente queremos testear es la interfaz gráfica, tal vez podamos desacoplar la interacción con la interfaz de la conexión con el resto del sistema. Esto se puede lograr fácilmente reemplazando la API con mocks y, de esta manera, restringiendo el scope de la prueba a probar la forma en que responde la pantalla frente a ciertas interacciones y respuestas. Adicionalmente, esto hace que las pruebas de UI sean mucho más rápidas. Una herramienta que usamos que nos resultó muy útil para reemplazar la API rápidamente es JSON Server, pero hay muchas más dando vueltas.
  2. Si, por el contrario, no nos interesa verificar como se renderiza cada pantalla podemos implementar las pruebas de integración sin pasar por la interfaz gráfica, utilizando un cliente HTTP que mande los pedidos directamente a la API y luego verificando la respuesta obtenida en cada caso. Martin Fowler llama a este tipo de pruebas tests subcutáneos. En nuestros equipos los hemos implementado exitosamente en proyectos de backend (desarrollados en C# o en Java) y tienen la ventaja de ser mucho más rápidos e infinitamente más estables que las pruebas que interactúan con las pantallas. Aparte, permiten incorporar las pruebas de integración a las métricas de cobertura de código.
  3. Otra alternativa es utilizar runners de pruebas unitarias, como jUnit, xUnit, nUnit, Jasmine, Karma, etc para ejecutar pruebas de integración de scope limitado. De esta manera agregamos a las pruebas unitarias algunos tests que verifican la interacción entre pocos componentes, que se ejecutan mucho más rápido.
  4. La razón para explorar todas estas alternativas parte de la comprensión de que no es necesario testear todos los caminos posibles de la misma manera, ya que eso tiene un impacto muy negativo en la performance de los tests y en su mantenibilidad. La pirámide de pruebas nos recomienda mantener al mínimo la cantidad de pruebas de integración punta a punta. Entonces, ¿cuáles elegimos para implementar? Lo más conveniente es alinearlas con las funcionalidades críticas del negocio, aquellos features sin los cuales el sistema no tiene razón de ser. De esta manera, además, resulta más valorada y compartida la tarea de cuidar la salud de esas pruebas.
  5. Quizás el consejo más importante es tener presente que las pruebas de aceptación y la pirámide de pruebas son conceptos ortogonales, para evitar caer en el error de implementar todas las pruebas de aceptación como pruebas E2E. Las pruebas de aceptación pueden implementarse en cualquier nivel de la pirámide, incluso al nivel de pruebas unitarias. Podemos usar múltiples herramientas para eso: el error común es esperar que una sola resuelva todas nuestras necesidades. En general, Cucumber en sus múltiples sabores para distintos lenguajes de programación suele ser un aliado fundamental a la hora de lograr que la definición de nuestras pruebas cuente con una narrativa que todo el mundo pueda entender y discutir. Y lograr de esa manera que las pruebas se conviertan en documentación ejecutable.

Glosario de tipos de pruebas: para asegurarnos la comprensión del artículo, sumamos algunas definiciones de los diferentes tipos de pruebas que se mencionan en él.

1- Pruebas de integración: permiten verificar el correcto ensamblaje entre los distintos componentes una vez que han sido probados unitariamente y comprobar si interactúan correctamente. Puede haber distintos grados de integración. Si la integración es de punta a punta (desde la interacción del usuario con la pantalla hasta el acceso a los servicios de más bajo nivel) se considera que es una prueba E2E.

2- Pruebas de aceptación: Tienen como fin comprobar si el software está preparado para que los usuarios puedan realizar las funciones y tareas para las que se diseñó. Su objetivo es validar de manera automática que se cumplen los requisitos del usuario.

3- Pruebas unitarias: consisten en aislar una parte del código y comprobar que funciona a la perfección. Son pequeños tests que validan el comportamiento de un objeto y la lógica.

4- Pruebas de regresión: se realizan para asegurarse de que los cambios sobre un componente del SW no generan comportamientos no deseados o errores en otros componentes no modificados.

Si querés seguir profundizando sobre el tema, te dejamos algunos links interesantes:

https://martinfowler.com/articles/is-quality-worth-cost.html
https://martinfowler.com/articles/practical-test-pyramid.html
https://testing.googleblog.com/2015/04/just-say-no-to-more-end-to-end-tests.html

(1) Pirámide de Test — MIke Cohn: En la base de la pirámide se ubican los test unitarios. Los test unitarios brindan feedback muy específico y rápido.
El extremo superior representa los test de usuario, aquellos que prueban la aplicación punta-a-punta a través de la interfaz de usuario.
El sector medio de la pirámide estará constituido por lo que que Cohn denomina service tests (testean las funcionalidades/servicios provistas por la aplicación por debajo de la interfaz de usuario)

--

--

Grupo Esfera SA
Grupo Esfera Blog

Hacemos software en forma ágil y escribimos sobre lo que hacemos. Más info en https://www.grupoesfera.com.ar/