Pest en PHP: el framework de testing moderno y expresivo, alternativa a PHPUnit

Pest es un framework de testing para PHP que corre sobre PHPUnit pero ofrece una API mucho más expresiva y limpia. En lugar de clases con métodos testXxx(), escribes tests como funciones globales con test() o it(). Los matchers de expect() son encadenables y más legibles que los assert*() de PHPUnit. Si ya conoces PHPUnit, puedes usar Pest desde el primer día.

Instalación

composer require pestphp/pest --dev
./vendor/bin/pest --init

Sintaxis básica: test() e it()

<?php
// tests/Unit/CalculadoraTest.php

// test() y it() son equivalentes; it() lee más natural en inglés
test('suma dos números positivos', function () {
    expect(suma(3, 4))->toBe(7);
});

it('lanza excepción con divisor cero', function () {
    expect(fn() => dividir(10, 0))->toThrow(DivisionByZeroError::class);
});

it('devuelve false cuando el array está vacío', function () {
    expect(tieneElementos([]))->toBeFalse();
});

Los matchers más útiles de expect()

<?php
// Tipos y valores
expect($valor)->toBe(42);           // === (tipo y valor)
expect($valor)->toEqual(['a' => 1]); // == (comparación laxa, arrays/objetos)
expect($str)->toBeString();
expect($n)->toBeInt();
expect($n)->toBeFloat();
expect($b)->toBeBool();
expect($arr)->toBeArray();
expect($obj)->toBeInstanceOf(DateTimeImmutable::class);

// Nulidad y vacío
expect($valor)->toBeNull();
expect($valor)->not->toBeNull();
expect($arr)->toBeEmpty();
expect($arr)->not->toBeEmpty();

// Strings
expect($str)->toContain('PHP');
expect($str)->toStartWith('Hola');
expect($str)->toEndWith('mundo');
expect($str)->toMatch('/^d{4}-d{2}-d{2}$/');

// Arrays
expect($arr)->toHaveCount(3);
expect($arr)->toHaveKey('nombre');
expect($arr)->toContain('PHP');
expect($arr)->toContainOnlyInstancesOf(Usuario::class);

// Numéricos
expect($n)->toBeGreaterThan(0);
expect($n)->toBeLessThanOrEqual(100);
expect($n)->toBeBetween(1, 10);

// Excepciones
expect(fn() => lanzaError())->toThrow(RuntimeException::class);
expect(fn() => lanzaError())->toThrow(RuntimeException::class, 'mensaje concreto');

Agrupar tests con describe()

<?php
describe('UsuarioService', function () {
    beforeEach(function () {
        $this->servicio = new UsuarioService(
            new InMemoryUsuarioRepositorio()
        );
    });

    describe('registrar()', function () {
        it('crea el usuario correctamente', function () {
            $usuario = $this->servicio->registrar('[email protected]', 'pass123');
            expect($usuario->email)->toBe('[email protected]');
            expect($usuario->id)->toBePositive();
        });

        it('lanza excepción si el email ya existe', function () {
            $this->servicio->registrar('[email protected]', 'pass123');
            expect(fn() => $this->servicio->registrar('[email protected]', 'otro'))
                ->toThrow(DomainException::class, 'ya registrado');
        });

        it('hashea la contraseña', function () {
            $usuario = $this->servicio->registrar('[email protected]', 'secreto');
            expect($usuario->passwordHash)->not->toBe('secreto');
            expect(password_verify('secreto', $usuario->passwordHash))->toBeTrue();
        });
    });

    describe('autenticar()', function () {
        it('devuelve el usuario con credenciales correctas', function () {
            $this->servicio->registrar('[email protected]', 'pass123');
            $usuario = $this->servicio->autenticar('[email protected]', 'pass123');
            expect($usuario)->toBeInstanceOf(Usuario::class);
        });

        it('lanza excepción con contraseña incorrecta', function () {
            $this->servicio->registrar('[email protected]', 'pass123');
            expect(fn() => $this->servicio->autenticar('[email protected]', 'mal'))
                ->toThrow(RuntimeException::class);
        });
    });
});

Datasets: reemplazar DataProviders de PHPUnit

<?php
// Con PHPUnit DataProvider: más verboso
// Con Pest datasets: mucho más limpio

it('valida emails correctamente', function (string $email, bool $esValido) {
    expect(esEmailValido($email))->toBe($esValido);
})->with([
    ['[email protected]',  true],
    ['invalido',         false],
    ['sin@dominio',      false],
    ['@sinusuario.com',  false],
    ['[email protected]',   true],
]);

// Dataset nombrado para mensajes de error más claros
it('calcula el IVA correctamente', function (float $precio, float $iva, float $esperado) {
    expect(calcularIva($precio, $iva))->toBe($esperado);
})->with([
    'precio cero'     => [0.0,   21.0, 0.0],
    'precio normal'   => [100.0, 21.0, 21.0],
    'IVA reducido'    => [100.0, 10.0, 10.0],
    'IVA superreducido' => [100.0, 4.0, 4.0],
]);

Ejecutar Pest

# Ejecutar todos los tests
./vendor/bin/pest

# Con cobertura de código
./vendor/bin/pest --coverage --min=80

# Solo un fichero o directorio
./vendor/bin/pest tests/Unit/CalculadoraTest.php

# Solo tests que fallan
./vendor/bin/pest --failed

# Watch mode (re-ejecuta al guardar)
./vendor/bin/pest --watch

# Mostrar tiempo de cada test
./vendor/bin/pest --profile

Pest vs PHPUnit: ¿cuándo elegir Pest?

  • Pest es ideal para proyectos nuevos o equipos que valoran la legibilidad.
  • Puedes mezclar tests Pest y tests PHPUnit en el mismo proyecto (Pest los ejecuta todos).
  • Si tu equipo ya tiene muchos tests en PHPUnit, puedes migrar gradualmente: los tests PHPUnit siguen funcionando sin cambios.
  • Laravel viene con Pest desde las versiones recientes como opción por defecto.
  • La salida de Pest en el terminal es más limpia y visual que la de PHPUnit.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP