Las pruebas demuestran comportamiento. No demuestran estructura, y la estructura es donde la inconsistencia vuelve a colarse seis endpoints después de que todo el mundo acordara el patrón. Para eso está el análisis estático: una segunda opinión que corre en cada commit y no se cansa de imponer la misma regla.
// tests/Feature/ArchTest.php
arch()->preset()->php();
arch()->preset()->security();
arch()->preset()->laravel();
it('checks that my jobs have suffix Job')
->expect('App\Jobs')
->toHaveSuffix('Job');
it('checks that my actions are final class')
->expect('App\Actions')
->toBeFinal();
it('checks that my actions extend nothing')
->expect('App\Actions')
->toExtendNothing();
Los presets de arch() de Pest atrapan las categorías de error que la revisión de código se salta porque son aburridas de comprobar a mano: una clase que debería ser final y no lo es, un job sin su sufijo, una acción que extiende calladamente una clase base que el patrón dice que no debería. Falla en voz alta en cuanto alguien rompe la convención, en lugar de esperar a que un revisor lo note en el pull request cuarenta y siete.
PHPStan, mediante Larastan a nivel 9, es la otra mitad. Nivel 9 significa nada de mixed implícito, ningún acceso a array sin tipar que no pueda verificar, ningún nullable tragado en silencio. Atrapa el fallo que una prueba sólo pillaría si dieras justo con la entrada que lo dispara.
Mantener la suite lo bastante rapida
Una suite que tarda veinte minutos no se ejecuta antes de cada commit. Se ejecuta antes de cada despliegue, y para cuando falla ya has olvidado qué cambiaste hace una hora. La velocidad no es un lujo: es la diferencia entre una suite que atrapa fallos y una que sólo se sienta en la integración continua alargando la construcción.
<!-- api-server phpunit.xml -->
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="CACHE_STORE" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
Cada una de esas líneas elimina una dependencia real de la ejecución. SQLite en memoria significa cero viajes a un servidor de base de datos. QUEUE_CONNECTION=sync significa que un job despachado corre en línea, en el mismo proceso, en lugar de esperar a un worker que de todos modos no está corriendo. Nada de esto es configuración exótica: es el entorno de pruebas estándar de Laravel, aplicado consistentemente.
El otro coste se esconde en la lógica de reintentos. La espera del trait compartido duerme de verdad entre reintentos en producción. Si la dejas correr, la prueba de reintentos quemaría segundos reales de reloj demostrando que esa lógica funciona:
// tests/Feature/SendsRequestsThrottleRetryTest.php
beforeEach(function () {
config()->set('webplo.throttle_retry.max_attempts', 3);
// Keep tests fast: clamp every backoff to zero so no real sleeping
// happens while still exercising the retry control flow.
config()->set('webplo.throttle_retry.base_delay_ms', 0);
config()->set('webplo.throttle_retry.max_delay_ms', 0);
});
Poner la espera a cero no se salta el reintento: sólo se niega a esperarlo. El conteo de intentos y el eventual abandono siguen ejecutándose y comprobándose. Sólo se elimina el dormir. Multiplica unos segundos reales de espera por una suite con cientos de pruebas y una suite de dos minutos se convierte en una de veinte, a cambio de cero confianza adicional. Rápido y exhaustivo no están reñidos. El truco es no hacer nunca que la máquina espere por algo que la prueba no necesita demostrar.