Testing con Pest PHP en Laravel

Nivel
Laravel
Laravel
Actualizado: 18/04/2026

¿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 - Autor del tutorial

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.