Ir al contenido

Por qué su batería de pruebas tarda once minutos

Una batería lenta es una batería que la gente se salta. El tiempo casi nunca está en las aserciones: está en reconstruir la base de datos, hashear contraseñas y salir a la red sin querer.

4 min de lectura

Una batería que tarda once minutos no se ejecuta antes de hacer push. La ejecuta CI, después de que se haya perdido el contexto, lo que convierte una corrección de cinco segundos en un viaje de ida y vuelta de veinte minutos. Al final alguien hace push sobre una compilación en rojo porque está bastante seguro de que no tiene que ver.

La velocidad de la batería no es, por tanto, una comodidad para el equipo. Es lo que decide si las pruebas hacen su trabajo.

Medir antes de adivinar

php artisan test --profile

Esto imprime las pruebas más lentas. Casi siempre un puñado de casos se lleva la mayor parte del reloj, y el arreglo es concreto y no arquitectónico.

La base de datos, que suele ser casi todo

Migrar en cada prueba. RefreshDatabase envuelve cada prueba en una transacción y la revierte, lo cual es rápido. DatabaseMigrations ejecuta todas las migraciones en cada prueba, lo cual no lo es. En un proyecto con doscientas migraciones, esa diferencia es el problema entero.

Migraciones en lugar de un volcado de esquema. Incluso una vez por ejecución, repetir doscientas migraciones tarda más que cargar su resultado:

php artisan schema:dump --prune

La batería carga entonces un solo fichero SQL. Es la mayor ganancia en una base de código madura y cuesta un comando.

Factories que crean más de lo que la prueba necesita. Una factory con una cadena de has() creando veinte registros relacionados para una prueba que lee un campo. Cree el mínimo, y use make() en lugar de create() donde nada toque la base de datos.

El hasheo de contraseñas, que nadie se espera

El hasheo es lento a propósito: para eso está. Una batería que crea cientos de usuarios mediante una factory paga ese coste cientos de veces.

// tests/TestCase.php o Pest.php
Hash::driver('bcrypt')->setRounds(4);

En baterías con muchos usuarios de prueba, esto solo ha partido tiempos de ejecución por la mitad.

Pruebas que salen a la red

La categoría más dañina, porque son lentas y poco fiables a la vez. Una prueba que llama a una API real espera al servidor de otra persona y falla cuando ese servidor tiene un mal día.

Http::preventStrayRequests();

Ponga eso en el caso de prueba base. Cualquier prueba que haga una llamada HTTP sin simular falla ahora al instante y le dice cuál, y se va a encontrar algunas que no sabía que existían.

Lo mismo vale para colas, correo y almacenamiento. Simulados son instantáneos; reales están haciendo un trabajo sobre el que la prueba no afirma nada.

Esperas y sondeos

Un sleep(2) en una prueba que espera algo asíncrono son dos segundos en cada ejecución, para siempre, y o sobra o - en una máquina más lenta - no basta.

Los ayudantes de tiempo de Laravel hacen innecesaria la espera: congele el tiempo, viaje hacia delante, afirme. Para trabajo genuinamente asíncrono, ejecute la cola de forma síncrona en las pruebas en lugar de esperar a un worker.

Las pruebas de navegador, que van en otro cubo

Son lentas porque conducen un navegador real, y no hay truco que haga eso rápido. La respuesta es la cantidad y no la velocidad: un número pequeño que cubra los caminos que importan, ejecutado aparte de la batería rápida, para que no se interpongan entre una persona y su realimentación.

El paralelismo, cuando las pruebas se lo merezcan

php artisan test --parallel

Mejora casi lineal en una máquina con varios núcleos, y va a dejar al descubierto todas las pruebas que dependían calladamente de otra. Ficheros de fixtures compartidos, ids fijos en el código, un registro que se da por existente porque lo creó una prueba anterior.

Esos fallos merece la pena tenerlos. Una prueba que solo pasa cuando se ejecuta después de otra no es una prueba, es una secuencia, y iba a fallar al azar en CI de todas formas.

El objetivo

Por debajo del minuto para la batería que la gente ejecuta antes de hacer push, con todo lo más lento - pruebas de navegador, integración contra servicios reales - en un trabajo aparte que se ejecute después.

La medida no es la cobertura ni la elegancia. Es si ejecutar las pruebas es algo que alguien hace sin tener que decidirlo.

Una suite que nadie ejecuta es también una suite detrás de la cual nadie puede actualizar. Por eso construirla es la primera fase de una actualización y no la última. Y si la suite rápida corre sobre SQLite mientras producción corre MySQL, ese intercambio tiene un coste, y no siempre es pequeño.

Preguntas relacionadas

¿Cómo de rápida debería ser una batería?
Lo bastante rápida como para que ejecutarla no sea una decisión. Por debajo del minuto la gente la ejecuta antes de cada push sin pensarlo; pasados los cinco empiezan a hacer push y a esperar a CI, lo que mueve la realimentación de segundos a minutos y cuesta mucho más que el tiempo ahorrado.
¿Es una base de datos en memoria la respuesta?
Es rápida y es una base de datos distinta de la que ejecuta en producción, lo que significa que no va a cazar la violación de restricción, el problema de colación ni la migración que bloquea. Razonable para una batería con mucha prueba unitaria; arriesgada como única cosa entre usted y un error de esquema.
¿Ayudan las pruebas en paralelo?
Bastante, y solo cuando las pruebas están lo bastante aisladas como para sobrevivirlo. Cualquier cosa que escriba en una ruta de fixtures compartida, en un id de registro fijo o en un servicio externo fallará de forma intermitente bajo paralelismo, lo cual conviene arreglar igualmente, porque esas son las pruebas que también fallan al azar en CI.
¿Dónde deberíamos mirar primero?
A las diez pruebas más lentas, no a la media. Las baterías suelen estar dominadas por un puñado de casos que hacen algo caro, y arreglar esos es una tarde. Reescribir la batería entera por velocidad, no.

← Volver a todos los artículos

Llamar+1 848 272 7583WhatsApp+90 850 308 5436Correoinfo@codefacture.comPágina de contacto