Saltar al contenido
Volver al blog
LaravelTestingPHP

Pest PHP: de cero a productivo en una tarde

Por qué cambié PHPUnit por Pest en todos mis proyectos y cómo montar una suite que de verdad se mantenga con el tiempo.

Ismael Catala2 min de lectura

Durante años escribí tests con PHPUnit y me parecía bien. Después probé Pest en un proyecto pequeño y ya no he vuelto. No es que PHPUnit sea malo: es que Pest elimina el ruido que te separa de la intención del test.

La diferencia en una pantalla

Esto es PHPUnit:

class PostTest extends TestCase
{
    public function test_a_guest_cannot_create_a_post(): void
    {
        $response = $this->post('/posts', ['title' => 'Hola']);
 
        $response->assertRedirect('/login');
    }
}

Y esto es exactamente lo mismo en Pest:

it('does not let a guest create a post', function () {
    $this->post('/posts', ['title' => 'Hola'])
        ->assertRedirect('/login');
});

Se lee como una frase. Cuando la suite tiene trescientos tests, esa diferencia deja de ser estética y pasa a ser económica.

Datasets: el arma secreta

Aquí es donde Pest se despega de verdad. Los datasets convierten cinco tests copiados y pegados en uno solo:

it('rejects invalid emails', function (string $email) {
    expect(fn () => User::create(['email' => $email]))
        ->toThrow(ValidationException::class);
})->with([
    'sin arroba' => 'ismaelexample.com',
    'sin dominio' => 'isma@',
    'vacío' => '',
    'con espacios' => 'isma @example.com',
]);

Cada caso se reporta por separado con su nombre, así que cuando falla sabes exactamente cuál sin leer una sola línea de código.

Higher order tests

Para las comprobaciones triviales puedes encadenar directamente:

it('has a home page')
    ->get('/')
    ->assertOk();

No abuses de esto. Funciona de maravilla para smoke tests y se vuelve ilegible en cuanto hay tres aserciones.

Cómo monto la suite

Mi tests/Pest.php casi siempre acaba así:

uses(Tests\TestCase::class, Illuminate\Foundation\Testing\RefreshDatabase::class)
    ->in('Feature');
 
expect()->extend('toBeSlug', function () {
    return $this->toMatch('/^[a-z0-9]+(?:-[a-z0-9]+)*$/');
});

Dos ideas detrás de esto:

  1. RefreshDatabase solo en Feature. Los tests unitarios no deberían tocar la base de datos; si uno la necesita, casi siempre es señal de que la lógica está en el sitio equivocado.
  2. Expectations propias para el dominio. toBeSlug, toBeValidInvoice, toBeWithinBusinessHours. El test acaba hablando el idioma del negocio en vez del idioma del framework.

Merece la pena

La migración desde PHPUnit es incremental: Pest ejecuta tus tests de PHPUnit sin tocarlos. Puedes instalarlo hoy, escribir los tests nuevos en Pest y no migrar nunca los viejos si no te apetece.

Esa es, probablemente, la mejor razón para probarlo.