¿Qué es Pest?
Pest es un framework de testing para PHP con una sintaxis fluida e inspirada en Jest (JavaScript). Desde Laravel 11, Pest es el framework de testing predeterminado en los nuevos proyectos (aunque PHPUnit sigue siendo compatible y ambos pueden coexistir).
# Pest viene preinstalado en Laravel 11+
# Para proyectos anteriores:
composer require pestphp/pest --dev --with-all-dependencies
php artisan pest:install
Sintaxis básica: test() e it()
En lugar de clases con métodos, Pest usa funciones globales test() o it():
// tests/Feature/ProductoTest.php
test('la página de productos devuelve 200', function () {
$response = $this->get('/productos');
$response->assertStatus(200);
});
it('muestra los productos en la página principal', function () {
$producto = Producto::factory()->create(['nombre' => 'Portátil HP']);
$response = $this->get('/productos');
$response->assertStatus(200)
->assertSee('Portátil HP');
});
test() e it() son equivalentes; it() se lee más natural en frases: "it shows products on the page".
Expectations: sintaxis fluida para aserciones
Pest introduce Expectations, una API fluida para verificar valores:
test('calcula el precio con descuento correctamente', function () {
$precio = calcularDescuento(100.0, 20); // 80.0
expect($precio)->toBe(80.0);
});
test('el usuario tiene rol de administrador', function () {
$admin = User::factory()->create(['rol' => 'admin']);
expect($admin->rol)->toBe('admin')
->and($admin->esAdmin())->toBeTrue()
->and($admin->email)->toContain('@');
});
test('la colección de productos no está vacía', function () {
Producto::factory()->count(3)->create();
expect(Producto::all())
->toHaveCount(3)
->each(fn ($item) => $item->nombre->not->toBeNull());
});
Matchers más usados
expect($valor)->toBe(42); // idéntico (===)
expect($valor)->toEqual(42); // igual (==)
expect($valor)->toBeTrue();
expect($valor)->toBeFalse();
expect($valor)->toBeNull();
expect($valor)->not->toBeNull();
expect($valor)->toBeInstanceOf(User::class);
expect($valor)->toContain('laravel');
expect($array)->toHaveCount(5);
expect($array)->toHaveKey('nombre');
expect($numero)->toBeGreaterThan(0);
expect($numero)->toBeBetween(1, 100);
expect($string)->toStartWith('https://');
expect($string)->toMatchPattern('/^\d{4}-\d{2}-\d{2}$/');
expect(fn() => funcionQueExplota())->toThrow(\Exception::class);
Tests de funcionalidad (Feature Tests) con Pest
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->usuario = User::factory()->create();
});
test('usuario autenticado puede ver sus pedidos', function () {
$pedido = Pedido::factory()->create(['user_id' => $this->usuario->id]);
$response = $this->actingAs($this->usuario)
->get('/mis-pedidos');
$response->assertStatus(200)
->assertSee($pedido->numero);
});
test('usuario no autenticado es redirigido al login', function () {
$response = $this->get('/mis-pedidos');
$response->assertRedirect('/login');
});
test('puede crear un pedido', function () {
$response = $this->actingAs($this->usuario)
->post('/pedidos', [
'producto_id' => Producto::factory()->create()->id,
'cantidad' => 2,
]);
$response->assertStatus(201);
expect(Pedido::count())->toBe(1);
expect(Pedido::first()->user_id)->toBe($this->usuario->id);
});
describe() para agrupar tests relacionados
describe('API de productos', function () {
uses(RefreshDatabase::class);
it('lista todos los productos', function () {
Producto::factory()->count(5)->create();
$response = $this->getJson('/api/productos');
$response->assertStatus(200)
->assertJsonCount(5, 'data');
});
it('crea un producto', function () {
$response = $this->actingAs(User::factory()->create(), 'sanctum')
->postJson('/api/productos', [
'nombre' => 'Nuevo producto',
'precio' => 29.99,
]);
$response->assertStatus(201)
->assertJsonPath('data.nombre', 'Nuevo producto');
});
it('devuelve 404 para producto inexistente', function () {
$response = $this->getJson('/api/productos/9999');
$response->assertStatus(404);
});
});
Datasets: tests parametrizados
dataset('emails válidos', [
'usuario@ejemplo.com',
'admin+test@sub.dominio.es',
'nombre.apellido@empresa.org',
]);
dataset('emails inválidos', [
'no-es-email',
'@dominio.com',
'usuario@',
'',
]);
it('valida emails correctamente', function (string $email) {
expect(filter_var($email, FILTER_VALIDATE_EMAIL))->not->toBeFalse();
})->with('emails válidos');
it('rechaza emails inválidos', function (string $email) {
expect(filter_var($email, FILTER_VALIDATE_EMAIL))->toBeFalse();
})->with('emails inválidos');
Tests de arquitectura con arch()
Pest Arch permite verificar convenciones de arquitectura del proyecto:
// tests/Architecture/ArchitectureTest.php
arch('los modelos extienden Model de Eloquent')
->expect('App\Models')
->toExtend('Illuminate\Database\Eloquent\Model');
arch('los controladores extienden el controlador base')
->expect('App\Http\Controllers')
->toExtend('App\Http\Controllers\Controller');
arch('los servicios no tienen dependencias de HTTP')
->expect('App\Services')
->not->toUse('Illuminate\Http\Request');
arch('los jobs implementan ShouldQueue')
->expect('App\Jobs')
->toImplement('Illuminate\Contracts\Queue\ShouldQueue');
arch('no hay uso de dd() ni dump() en el código')
->expect('App')
->not->toUse(['dd', 'dump', 'var_dump', 'print_r']);
beforeEach y afterEach
describe('Servicio de pagos', function () {
beforeEach(function () {
$this->servicio = new PagoService(
stripe: Mockery::mock(StripeClient::class)
);
});
afterEach(function () {
Mockery::close();
});
it('procesa un pago correctamente', function () {
$this->servicio->stripe
->shouldReceive('cobrar')
->once()
->andReturn(['status' => 'succeeded']);
$resultado = $this->servicio->procesar(100.0, 'tok_visa');
expect($resultado->exitoso())->toBeTrue();
});
});
Comparación Pest vs. PHPUnit
| Aspecto | PHPUnit | Pest |
|---------|---------|------|
| Sintaxis | Clases con métodos | Funciones test() / it() |
| Aserciones | $this->assertEquals(...) | expect(...)->toBe(...) |
| Agrupación | Clases | describe() |
| Datos parametrizados | @dataProvider | ->with(...) |
| Tests de arquitectura | No incluido | arch() |
| Instalación en L11+ | Disponible | Predeterminado |
| Compatibilidad | — | Compatible con PHPUnit |
Ambos son completamente válidos. La tendencia en la comunidad Laravel es migrar progresivamente a Pest por su sintaxis más legible y sus capacidades de tests de arquitectura.
Fuentes y referencias
Documentación oficial y recursos externos para profundizar en Laravel
Documentación oficial de Laravel
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Laravel es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Laravel
Explora más contenido relacionado con Laravel y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Entender qué es Pest y por qué se usa en proyectos Laravel modernos. Escribir pruebas de funcionalidad con la sintaxis concisa de Pest. Usar Expectations de Pest para aserciones fluidas y legibles. Organizar tests con describe() y agrupar casos con dataset(). Crear tests de arquitectura con Pest Arch para verificar convenciones del proyecto. Migrar tests PHPUnit existentes a sintaxis Pest.