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.
