CSI: Flaky Tests ¿Cómo investigar fallos intermitentes sin perder la cabeza?
Curiosamente todos mis proyectos recientes han sido “arreglar suites que fallan” y que no se sabe por qué fallan.
Un mismo test a veces pasa y a veces falla, o pasan localmente y fallan en CI, mientras más workers (instancias) están corriendo más alta es la tasa de fallos. Esta es, exactamente la definición de un flaky test. Cuando, ante el mismo código y el mismo entorno, a veces pasa y a veces falla.
Y el propósito de las pruebas es tener feedback efectivo y rápido de lo que estamos entregando. Y los flaky tests nos alejan del propósito así:
Pérdida de confianza:
Los desarrolladores empiezan a ignorar los fallos reales (“ahí está fallando de nuevo el mismo test, vamos a correrlo de nuevo”). Y esto, erosiona la confianza de los desarrolladores en las pruebas que escribimos y seamos honestos, para estos son las pruebas, para tener confianza en lo que estamos entregando a los clientes y usuarios en cada release.
Puede llegar el momento en que empecemos a ignorar los resultados de las pruebas para lanzar una nueva versión de nuestro producto ”rápido” y podríamos estar dejando escapar errores.
Lentitud en el ciclo:
Y si no empezamos a ignorar los resultados de las pruebas en función de releases más rápidos, nos encontramos en el otro extremo, re-ejecutando las pruebas una y otra y otra vez hasta que pasan, haciendo que el trabajo en general (no solo los releases) sean más lentos. Desde pull Requests que quedan bloqueados (que no podemos hacer merge hasta que los tests pasan y los checks del PR están ok) hasta retrasos en nuestros deadlines.
Re-ejecutar el CI/CD consume tiempo y dinero.
El tradeoff de forzar los tests a que pasen:
También en el otro extremo he visto (y quisiera no haber visto) tests forzados a pasar, que no tienen verificaciones/assertions relevantes y algunos, mucho peores, que no verifican nada, con un mal diseño (orientado a correr de forma automática y rápida pero no a detectar errores). Estos tests no reflejan el estado verdadero del sistema.
¿Cómo estar seguros que un test o un grupo de tests son flaky?
Lo principal es que el test falla intermitentemente:
- Pasa en local pero no en CI, o al revés.
- Pasa cuando lo corres solo (aislado) pero falla cuando se corre toda la suite de regresión (o un grupo de tests), o al revés.
- Fallan más conforme aumentamos la cantidad de workers o instancias (en paralelo).
- Falla solo la primera vez y pasa cuando re-ejecutas.
- Falla más en ciertos horarios.
- Falla solo en headless mode o al revés (headed mode).
¿Por dónde empezamos a diagnosticar?
Algunas ideas (basadas en mi experiencia):
- Reproducir el fallo consistentemente (correr en loop), especialmente si estamos corriendo la suite completa.
- Aislar el test, para evaluar si pasa cuando se corre solo (aislado).
- Agregar logging estratégico, es súper útil hacer ”print” de estados, IDs (que podemos revisar luego en la base de datos), respuestas del API, qué data estamos enviando al API. A veces puede parecer mucha data pero, a fines de evaluar por qué nuestro test es flaky, nos da muchísima información que podemos usar para arreglar estos fallos. No tienes que hacerlo con todos los tests pero sí con los que están bajo investigación.
- Revisar logs de la aplicación (y API), no solamente de las pruebas, a veces solo en los logs de la aplicación podemos encontrar la causa verdadera del error (por ejemplo: estás tratando de crear un registro duplicado, hay un fallo en un cambio de estado, no se puede borrar algo, el usuario no tiene permiso para ejecutar tal operación, no tienes acceso a un determinado recurso, una transacción está bloqueada).
- Revisar los screenshots/videos del fallo, si el test es de interfaz de usuario, podemos ver cuál era el estado exacto (en la interfaz) cuando falló el tests, qué data estaba usando, qué usuario estaba logueado, y las diferencias entre una ejecución y otra.
- Analizar timing con traces, que nos permite detectar en qué punto específico el test podría estar tardando más de lo esperado (esperando una respuesta o estado, o que un elemento o dato exista).
- Analizar la data y estados que usa el test solo y la suite completa. Sé que suena a muchísimo trabajo pero a veces es necesario para entender qué pasa.
Errores comunes que sí podemos arreglar en el test/suite
En el análisis anterior puede que encuentres que varios tests están usando la misma data: creando, modificando y/o eliminando el mismo registro en la base de datos. Varios tests modificando un estado de la misma entidad/registro. Quizás están intentando crear más de un registro con el mismo nombre? Aunque sea solo en modo lectura, tenemos 50 tests usando el mismo registro de base de datos?
DOM (elementos en la interfaz de usuario) que no carga lo suficientemente rápido (como el test espera).
Un test depende de otro para ejecutarse, por ejemplo, un test para editar los permisos de un usuario que no existe previamente sino que depende que otro test crea ese usuario.
Y cómo los arreglamos?
| Error | Causa | Fix |
|---|---|---|
| Varios tests usando la misma data | El test depende de datos dejados por un test anterior o cambia el mismo registro de estado o distintos valores alterando el resultado esperado del test. | Cada test debe tener su propia data, ya sea que la cree antes del test y la elimine después del test, o que sea data pre-cargada en la base de datos, o usemos mock data. No importa tanto el método que elijamos pero es importante que cada test sea independiente de otros, a efectos de paralelización (que varios tests estén corriendo al mismo tiempo). |
| DOM que no carga lo suficientemente rápido | Servidor o servicio lento. Muchos tests corriendo al mismo tiempo llevando al servidor/servicio al límite. | Agregando timeouts dinámicos (que el elemento exista, que sea visible, que sea interactable, entre otros), que esperan por elementos de UI hasta, máximo el tiempo que especificamos.Por ejemplo, si es de 15 segundos, es hasta 15 segundos máximo, si el elemento aparece antes, el test continúa al siguiente paso antes, en vez de esperar exactamente 15 segundos. Con esto nos aseguramos de esperar tiempo suficiente por un elemento sin ralentizar la ejecución más de lo estrictamente necesario. |
| Un test depende de otro para ejecutarse | El test depende de datos dejados por un test anterior. El test a veces pasa pero es porque encuentran datos de otro test. | De nuevo, cada test debe tener su propia data. En algunas ocasiones, si tienes un test que crea data, otro que la edita y otro que la elimina, puedes configurarlos para que corran de forma secuencial, pero con ésto, sacrificas paralelización. Puedes evaluar cuál es tu necesidad más inmediata, velocidad de ejecución o confianza en el test (ambos son importantes). |
Errores comunes que NO podemos arreglar (nosotros solos) en el test/suite
Si los tests corren más lento conforme agregamos más workers/instancias y fallan más estamos ante requests rate limits o una sobrecarga del servidor.
Fallos por requests rate limit: un número de requests al API y/o servidor/servicio mayor al permitido (configuraciones de seguridad) por cada usuario, por período de tiempo, por IP. Esto requiere que los desarrolladores/DevOps nos ayuden incrementando estos límites en los servidores. Muchos errores de paralelización (varios tests corriendo en paralelo en el mismo servidor/servicio) se deben a esto.
Sobrecarga del servidor: a veces nuestras pruebas (si son muchos tests) llevan el servidor al límite. Los ambientes de prueba muy pocas veces son los mismos que producción (recursos como memoria RAM, espacio en disco) por temas de presupuesto. Otras veces las pruebas ejercen una carga mayor a la que sucedería en producción (uso real por parte de usuarios y clientes).
Fallos por servicios externos: un API o servicio externo que esté fuera del control de nuestro equipo.
Respuestas 500 intermitentes: esto tiene que ser evaluado por los desarrolladores.
Errores de Red: son los más difíciles de diagnosticar y corregir, porque, por naturaleza son intermitentes y dependen de factores externos al código del test y de la aplicación misma. Errores como ECONNRESET, ETIMEDOUT, ECONNREFUSED como respuestas a llamadas de API y servicios es un indicio claro que estamos ante un error de red. Depende de configuraciones de servicios de nube y de región.
Race Conditions y Procesos Asíncronos
Una Race Condition ocurre cuando el resultado de un test depende de la secuencia o el timing de eventos incontrolables.
Un test automatizado va mucho más rápido que como sucedería si lo estuviésemos ejecutando manualmente, y por lo tanto, hay errores/issues en la ejecución automatizada que no se pueden reproducir manualmente (y aquí es donde se vuelve más importante aún revisar videos/screenshots del error o el logging de cada request/paso/verificación del test), esto ocasiona que el test falle for timeout: por elementos que no aparecen en el tiempo esperado, data que no carga en la interfaz de usuario en el tiempo esperado, o requests que no responden en el tiempo esperado (por la cantidad de data que traen o algún problema en el API).
Esto sucede porque nuestros tests ejecutan pasos (líneas de código) en nanosegundos mientras las aplicaciones tienen que renderizar HTML, descargar CSS, ejecutar JavaScript y esperar respuestas del servidor. Esto toma milisegundos o segundos y es lo que llamamos proceso “asíncrono”. Y como el framework y la aplicación no siempre van al mismo ritmo, aquí nace el race condition.
Algunas soluciones que podemos implementar son:
Tiempos de espera dinámicos (descrito en la tabla 1) para esperar por elementos en la interfaz de usuario o una respuesta del API.
Y más allá de esperar elementos, si esperamos algo como un cambio de estado y sabemos que puede tardar hasta 5 minutos, por ejemplo, en vez de esperar 5 minutos y obtener el estado para compararlo con lo que esperamos (verificación o assertion) podemos hacer una función poll que revise cada cierto tiempo (cada minuto, por ejemplo) si el estado es el que esperamos con un tiempo máximo de 7 minutos. Si al cabo de los 7 minutos falla (no llega a suceder el estado que esperamos), entonces el test falla.
Control de la data: Si tus tests agregan y eliminan data por sí mismos, busca la forma más eficiente/rápida de completar este proceso, a veces el test falla antes de iniciar la ejecución a la espera de la creación de data. Hay otras opciones como pre-cargar la data en la base de datos (que también implica que debe existir un ”estado inicial de esa data” y que en cada ejecución de la suite la data debe estar en su estado inicial) o usar mocks y stubs (que ya viene cubierto técnicamente hablando por distintos frameworks de automatización de pruebas y no tienes que construir módulos y librerías para poder usar mock data).
Si los tests crean la data, asegúrate que no tengan los mismos nombres y códigos (parece tonto pero pasa y mucho). Y a la vez, no crees más data de la estrictamente necesaria.
Conclusión
Investigar y corregir flaky tests requiere paciencia, análisis y métodos.
Un test flaky puede ser síntoma de problemas muy diversos: desde errores en el diseño del test (data compartida, dependencias entre tests, esperas inadecuadas) hasta situaciones que escapan de nuestro control directo (rate limits, sobrecarga del servidor, servicios externos).
Lo más importante es:
- No ignores el fallo: Cada re-ejecución manual es tiempo y dinero perdido, y cada fallo ignorado es un bug potencial que dejamos escapar.
- No culpes al test o al sistema bajo testing sin evidencia: A veces el test es sólo el mensajero de un problema real en el sistema. Antes de “arreglarlo” para que pase, investiga si está revelando algo que también afecta a los usuarios.
- Aísla, reproduce, documenta: Tu mejor (y casi siempre la única) herramienta es la evidencia: logs, screenshots, traces, y sobre todo, la capacidad de reproducir el fallo y analizar toda la información en conjunto.
- Colabora: en los casos que no podemos arreglar por nosotros mismos (race conditions, límites de requests, problemas de infraestructura y red) necesitamos al equipo de desarrollo y DevOps.
Los flaky tests no desaparecen por arte de magia, pero con las técnicas correctas podemos transformar una suite caótica en una que realmente cumpla su propósito: darnos confianza en lo que entregamos.

