2 de septiembre de 2026

Pest 5 y PHPUnit 13: qué se rompe al actualizar (y qué falla en silencio)

Foto de Marco Orta Marco Orta | 12 min de lectura
Compartir
Render 3D de un matraz de laboratorio de vidrio violeta con líquido verde brillante, agrietado por un costado y goteando lentamente
Tabla de Contenidos

    Pest 5 salió a finales de julio de 2026, corre directamente sobre PHPUnit 13 y exige PHP 8.4 como mínimo sin excepciones. Pest en sí mismo no trae rupturas a nivel de API — todo el dolor viene heredado de las reglas de mocking más estrictas de PHPUnit 13. Y lo que de verdad le pega a los equipos no es un error fatal: es que PHPUnit 13 convierte varios patrones de mocking comunes en avisos de deprecación en vez de fallas, así que tu suite puede quedar completamente en verde mientras deja de verificar, en silencio, justo lo que creías que verificaba. Si estás en Laravel 12, hay un segundo problema más contundente: el plugin oficial de Pest para Laravel en su versión 5 pide Laravel 13.23 o superior, así que el plugin ni siquiera se instala.

    ¿De dónde vienes?

    El esfuerzo del salto no es parejo. Depende de cuántas versiones mayores de PHPUnit te estés saltando de un solo golpe.

    • Pest 4 (PHPUnit 12) → Pest 5 (PHPUnit 13). Es el salto pensado, el más pequeño. Pest 4 ya pedía PHP 8.3, así que el único piso que subes es de 8.3 a 8.4. Absorbes una sola tanda de cambios de PHPUnit (12 → 13).
    • Pest 3 o anterior (PHPUnit 11 o más viejo) → Pest 5. Aquí absorbes dos tandas apiladas: PHPUnit 11 → 12 ya había eliminado los metadatos por docblock (necesitas atributos nativos de PHP: #[Test], #[DataProvider], etc.) y los mocks de clases abstractas o traits, encima de todo lo que cambia PHPUnit 13 más abajo. Saltar de 3 a 5 en un solo composer update es como una actualización de 20 minutos se convierte en una de dos días — pasa primero por 4, o al menos prueba el salto a 12 de forma aislada.
    • PHPUnit puro, sin Pest, en PHPUnit 12.5. La propia regla de actualización de PHPUnit es explícita: no muevas a 13 si tu suite todavía no corre limpia en 12.5. Si phpunit --display-deprecations muestra algo, arréglalo primero, en su lugar, antes de tocar la restricción de versión de Pest o PHPUnit.

    El piso que no se negocia: PHP 8.4

    Pest 5 exige PHP 8.4.0 o superior, punto. No hay un “en la práctica funciona en 8.3” como sí existe en algunos saltos de versión de Laravel. Si tu equipo sigue en PHP 8.1, 8.2 u 8.3, eso es trabajo de infraestructura que pasa antes de tocar el composer.json de Pest — no en paralelo.

    php -v          # necesitas 8.4.0 o superior
    

    Haz el salto de PHP como su propio despliegue, verificado en verde con el stack de pruebas viejo, antes de cambiar también el stack de pruebas. De lo contrario, un CI en rojo después de la actualización no te dice cuál de los dos cambios fue el culpable.

    Rupturas, ordenadas por lo que de verdad duele

    1. Laravel 12 no puede usar el plugin de Pest 5 para Laravel (bloqueante, no silencioso)

    Esta es la que nadie menciona cuando venden la actualización como “solo una línea”, y es lo primero que detiene en seco a un equipo de Laravel. pestphp/pest-plugin-laravel en su versión 5.0.1 declara:

    {
      "require": {
        "php": "^8.4",
        "laravel/framework": "^13.23.0",
        "pestphp/pest": "^5.0.1"
      }
    }
    

    Si tu app está en Laravel 12, composer update se va a negar a resolver pest-plugin-laravel ^5.0 — Composer reporta un conflicto, no una falla de prueba misteriosa. La solución no es un problema de Pest: necesitas Laravel 13.23+ antes de que el plugin de Laravel para Pest 5 sea siquiera instalable. Si estás actualizando ambos frentes a la vez, ordénalo — primero Laravel, después Pest — o te vas a pasar una tarde depurando un árbol de dependencias que nunca iba a resolver.

    2. any() y with*() sin expects() — la suite que pasa pero deja de verificar (silencioso)

    Este es el cambio que más importa, porque nada en tu salida de CI te avisa que ocurrió. PHPUnit 13 deprecra de forma dura dos patrones que antes eran la forma cómoda y por defecto de configurar un mock:

    // Antes: funciona, y significa en silencio "cualquier número de llamadas, sin verificar orden"
    $mock->method('handle')->with($this->equalTo($payload));
    
    // PHPUnit 13: emite una deprecación, pero igual corre y sigue "pasando"
    

    Las deprecaciones en PHPUnit son avisos, no fallas, a menos que configures explícitamente failOnDeprecation (o failOnPhpunitDeprecation, la propia de PHPUnit) en tu suite. Lo que significa que el escenario realista no es “la actualización rompe el CI el día uno” — es “el CI se queda en verde durante semanas mientras acumula decenas de avisos de deprecación que nadie lee, y después alguien activa el fallo estricto por deprecación, o PHPUnit 14 elimina el patrón de golpe y la suite se pone roja toda junta, en una fecha que tú no controlas”.

    La corrección es hacer explícita la intención en vez de depender del comportamiento implícito por defecto:

    // Si de verdad quieres verificar que la llamada ocurre:
    $mock->expects($this->once())->method('handle')->with($this->equalTo($payload));
    
    // Si no te importa si se llama o no — usa un stub, no un mock:
    $stub = $this->createStub(HandlerInterface::class);
    $stub->method('handle')->willReturn($result);
    

    El propio matcher any() de conteo de invocaciones queda deprecado de forma dura por la misma razón: combinar expects($this->any()) con un mock es una contradicción — los mocks existen para verificar interacciones, y any() significa “no me importa si esto pasa”. Los propios mantenedores de PHPUnit lo dicen sin rodeos: si no quieres verificar nada, lo que querías era un stub.

    3. createPartialMock() de Laravel choca con la regla #2 (silencioso, específico de Laravel)

    Es la misma deprecación de arriba, pero vale la pena señalarla aparte porque golpea a un helper que la mayoría de las suites de Laravel usan sin pensarlo dos veces. El trait de testing de Laravel envuelve el createPartialMock() de PHPUnit, y la implementación interna de ese envoltorio dispara la deprecación de “with*() sin expects()” en PHPUnit 13 aunque tu código de prueba se vea perfectamente sano:

    // Dispara la deprecación en PHPUnit 13, aunque se vea inocente
    $class = $this->createPartialMock(NightlyCommand::class, ['runSequenceOfCommands']);
    
    // Corregido: te saltas el envoltorio y usas el builder de PHPUnit directamente
    $class = $this->getMockBuilder(NightlyCommand::class)
        ->onlyMethods(['runSequenceOfCommands'])
        ->getMock();
    

    Búscalo con grep antes de actualizar, porque el número de deprecaciones puede ser grande si el patrón es común en tu suite:

    grep -rln "createPartialMock" tests/
    

    4. Eliminaciones reales — estas sí fallan, de inmediato, sin ambigüedad

    A diferencia de las dos anteriores, estas ya venían deprecadas de forma dura desde PHPUnit 12 y en 13 simplemente ya no existen. Lanzan un Error fatal, no un aviso, así que salen a la luz desde la primera corrida — que en realidad es la categoría fácil de resolver:

    // Eliminado en PHPUnit 13 — error fatal, no una deprecación
    Assert::isType('string', $value);
    $this->assertContainsOnly('string', $collection);
    $this->assertNotContainsOnly('int', $collection);
    
    // Sustituye las verificaciones estilo containsOnly() por las nuevas
    // aserciones de arreglos, o recurre a comprobaciones de tipo explícitas
    self::assertContainsOnlyInstancesOf(string::class, $collection); // si aplica
    

    También se eliminó el flag de CLI --dont-report-useless-tests, Configuration::includeTestSuite() / excludeTestSuite(), el atributo #[CoversNothing] en métodos individuales (ahora solo a nivel de clase), el atributo #[RunClassInSeparateProcess], y el soporte para cadenas de restricción de versión sin un operador de comparación explícito. Ninguno de estos es común en una suite típica de Laravel, pero si mantienes un paquete con extensiones propias de PHPUnit o herramientas de CI que invocan phpunit directamente, revisa tus flags.

    Si vienes de withConsecutive() específicamente: eso ya es viejo — se eliminó desde PHPUnit 10 — pero PHPUnit 13 por fin trae reemplazos decentes en vez de dejarte escribiendo workarounds incómodos de conteo de llamadas:

    // Nuevo en PHPUnit 13
    $mock->expects($this->once())
        ->method('handle')
        ->withParameterSetsInOrder(['first'], ['second']);
    
    $mock->expects($this->exactly(2))
        ->method('handle')
        ->withParameterSetsInAnyOrder(['first'], ['second']);
    

    5. El Test Impact Analysis necesita un driver de cobertura, o simplemente no corre (silencioso)

    La función estrella de Pest 5 — el Tia Engine, que promete reducir una suite de Laravel de 10 minutos a unos 4 segundos en corridas posteriores al solo volver a correr las pruebas afectadas por tu diff — necesita PCOV o Xdebug instalado para grabar su grafo de dependencias inicial. Sin uno de los dos, Pest no lanza error ni avisa fuerte; TIA simplemente nunca se activa, y tu suite sigue corriendo completa cada vez, en silencio. Si activaste TIA esperando la mejora de velocidad y no pasó nada, revisa si tienes un driver de cobertura antes de asumir que la función está rota:

    php -m | grep -iE "pcov|xdebug"
    

    Para una configuración de equipo, el patrón sensato es que el CI grabe el grafo base una vez por cada merge a main (donde activar un driver de cobertura sale barato) y que las máquinas locales consuman ese grafo en caché, en vez de que cada desarrollador corra una línea base instrumentada con cobertura en su propia máquina.

    6. Sin cambios de configuración — de verdad no hay nada que hacer aquí

    Vale la pena decirlo de forma explícita porque la gente lo espera: la guía de actualización de Pest 5 documenta ningún cambio en phpunit.xml ni en ningún archivo de configuración de Pest. Si tu suite corre limpia bajo las reglas de PHPUnit 13, el bump de versión en composer.json es toda la migración. Eso es lo bastante raro en este ecosistema como para no darle más vueltas de las necesarias.

    ¿Cuándo actualizar?

    • Estás en Pest 4 / PHPUnit 12, PHP 8.4 ya es tu piso, y phpunit --display-deprecations sale limpio. Actualiza ahora. Esto se acerca a los “2 minutos” que promete la documentación, y solo TIA ya vale la pena si tu suite pasó de un par de miles de pruebas.
    • Estás en Pest 4 pero todavía en PHP 8.3. Sube PHP primero, verifica que la suite actual siga en verde y sin deprecaciones en 8.4, y luego sube Pest como un paso aparte.
    • Estás en Pest 3 o en PHPUnit 11 o anterior, sin Pest. No te saltes directo a 5. Pasa primero por la tanda de PHPUnit 12 (atributos en vez de docblocks, sin mocks de abstractas ni traits), confirma que la suite está limpia, y toma el salto de 12 a 13 aparte.
    • Eres un equipo de Laravel todavía en Laravel 12. El plugin de Laravel para Pest 5 está fuera de tu alcance hasta que estés en Laravel 13.23+. O actualizas Laravel primero, o te quedas en pest-plugin-laravel ^4.0 con Pest 4 hasta que lo hagas — no dejes que el resolvedor de dependencias de Composer tome esa decisión por ti a medio camino.
    • Cualquiera con uso pesado de mocks en la suite, sin importar la versión. Corre el grep de deprecaciones antes de tocar siquiera el composer.json. El costo real de esta actualización no es el bump de versión — es descubrir cuántos de tus mocks solo “pasaban” porque en realidad no estaban verificando nada.

    Para seguir leyendo:

    Preguntas frecuentes

    Compartir

    Buscar

    Etiquetas

    PHP Laravel IA Tutorial JavaScript Migración Desarrollo Web Buenas Prácticas Upgrade Seguridad Laravel 13 Backend SEO Claude Herramientas