Cómo reducir el tiempo ejecución de nuestros tests

Si tienes la “suerte” (y espero que así sea!) de trabajar, o haber trabajado, en un sitio donde el testing es considerado ciudadano de primera seguro que habrás tenido que mantener una suite de cientos de tests que de manera progresiva y sutil tardan cada vez más.

Hoy quiero hablaros de un par de estrategias para mejorar este tiempo de ejecución en nuestros tests.

Cómo reducir el tiempo ejecución de nuestros tests

Hoy en este post me gustaría hablaros de un par de estrategias para conseguir reducir el tiempo que tardan en pasar nuestros tests utilizando un par de estrategias diferentes:

  • Ejecutar nuestros tests en paralelo haciendo uso de los cores de nuestras máquinas.
  • Ejecutar únicamente los tests que se ven afectados por los cambios después de salvar nuestros ficheros.

Para implementar estar estrategias haremos uso de Pytest como runner y una serie de plugins para el mismo.

Pytest

Cada día tengo más claro que Pytest es mi runner por defecto para tests. Pese a explorar otras alternativas como Mamba o Behave al final siempre acabo encontrando un plugin para Pytest que hace exactamente lo que necesito, de forma más sencilla y super-integrado con lo que ya tenía.

Para utilizar Pytest en nuestro proyecto es tan fácil como añadir la dependencia y ejecutar pytest .. Una opción del mismo que me encanta es pytest -ra . donde -ra que nos mostrará información extra para todos los tests que no pasen y haciendo caso omiso de los tests que por otra parte si han pasado, lo que nos va a permitir tener una visión mejor de porque fallan los tests.

Por último es muy habitual tener una comando para nuestros tests unitarios, integración, aceptación, etc., esto se puede lograr fácilmente con los siguientes comandos, ya que Pytest aceptan rutas a carpetas como parámetro:

  • pytest -ra tests/unit
  • pytest -ra tests/integration
  • pytest -ra tests/acceptance

Plugins

Lo que para mí hace que Pytest sea diferente al resto de opciones es su sistema de plugins.

De una forma supersencilla (tan sencillo como instalar una nueva dependencia en nuestro proyecto) podremos añadir flags a nuestros comandos definidos en el párrafo anterior.

Si tienes interés en ver que opciones hay disponibles aquí existe una lista bastante completa de plugins disponibles, de esa lista hoy os quiero hablar de los siguientes plugins:

Xdist

En la actualidad es bastante habitual disponer de portátiles con muchos núcleos, ¿por qué no aprovecharnos de ello?

Este plugin nos va a permitir ejecutar en paralelo todos nuestros tests lo que va a permitir reducir el tiempo de ejecución drásticamente. Esto aplica también a nuestros CIs donde es posible que la mejora sea mayor aun.

Para ejecutar nuestros tests usando este nuevo plugin bastaría con actualizar nuestros comandos tal que así:

  • pytest -n auto -ra tests/unit
  • pytest -n auto -ra tests/integration
  • pytest -n auto -ra tests/acceptance

El flag -n es una abreviatura de --numprocesses y que en auto tratara de utilizar tantos como haya disponibles.

Un punto muy interesante de este plugin y que es bastate común al empezar a usarlo es que veamos tests que fallar que antes no lo hacían.

Esto se debe sobre todo a tests de integración o aceptación que antes se hacían de forma secuencial (estoy pensando en un test para insertar un usuario y otro test a continuación para borrar ese mismo usuario), para estos casos xdist nos proporciona un decorador @pytest.mark.xdist_group(name="group_name") el cual debemos añadir a nuestros tests que estén relacionados entre sí:

class TestBoto3AwsRepositoryIntegration:
    @pytest.mark.xdist_group(name="aws")
    def test_adds_an_aws_user(self) - None:
	pass

    @pytest.mark.xdist_group("aws")
    def test_deletes_an_aws_user(self) - None:
        pass

  • pytest -n auto --dist loadgroup -ra tests/unit
  • pytest -n auto --dist loadgroup -ra tests/integration
  • pytest -n auto --dist loadgroup -ra tests/acceptance

Con estos cambios ya no deberíamos tener ninguna condición de carrera entre nuestros tests.

Testmon

Este segundo plugin a diferencia de xdist es una opción recomendable únicamente para usarla en local y no en nuestros CI. La estrategia seria hacer uso de testmon durante la fase de desarrollo y usar xdist en nuestro CI donde los recursos son mucho mayores.

Su funcionamiento se basa en entender nuestro código (los tests también son código xD) y ejecutar únicamente los tests que se ven afectados por los cambios que hemos introducido. Para ello se apoya en Coverage.py, una librería que se encargara de analizar las dependencias entre nuestros tests y nuestras clases.,

Para ejecutar nuestros tests usando este nuevo plugin bastaría con actualizar nuestros comandos tal que así:

  • pytest --testmon -ra tests/unit
  • pytest --testmon -ra tests/integration
  • pytest --testmon -ra tests/acceptance

Cuando se lanza el comando se ejecutaran los tests por primera vez y si salvamos un fichero cualquier sin hacer cambios y los volvemos a lanzar veremos que los tests no se vuelven a ejecutar.

Por último, este plugin genera un fichero llamado .testmondata que debemos incluir en nuestro .gitginore para evitar subirlo a nuestro repositorio.

Extra: Ejecutar nuestros tests automáticamente cada vez que salvemos

Ya como un extra me gustaría hablaros de un plugin muy interesante si en vuestro trabajo hacéis Test Driven Development.

Watch

Con pytest-watch podremos ejecutar de forma supersencilla nuestros tests en cada salvado. Si además le sumamos a la ecuación cualquiera de los dos plugins anteriores (xdist o testmon) el feedback será prácticamente instantáneo.

Para ejecutar nuestros tests usando este nuevo plugin bastaría con actualizar nuestros comandos tal que así:

  • ptw -p -- --testmon -ra tests/unit | ptw -p -- -n auto --dist loadgroup -ra tests/unit
  • ptw -p -- --testmon -ra tests/integration | ptw -p -- -n auto --dist loadgroup -ra tests/integration
  • ptw -p -- --testmon -ra tests/acceptance | ptw -p -- -n auto --dist loadgroup -ra tests/acceptance

Este plugin permite además ejecutar un comando de tu shell cuando los tests pasan, tanto en caso positivo como negativo. El caso más sencillo puede ser usar el comando say pero podremos llegar a enviar notificaciones i3 (como es mi caso) o a nuestro sistema operativo (indicaciones aquí).

Conclusiones

Y por mi parte nada más, espero que os haya gustado el post y que podáis sacar algo de valor del mismo.

¡Mantener nuestra suite de tests en un tiempo bajo debería ser una de nuestras mayores preocupaciones, ya que cuanto más rápido sea el feedback mejor!

Un saludo