> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Pruebas avanzadas con Pest

> Explicación sistemática del uso avanzado de Pest, el framework por defecto desde Laravel 11: Expectation API, datasets, fakes y Mockery.

## Qué es Pest

[Pest](https://pestphp.com) es un framework de tests construido sobre PHPUnit que se adopta por defecto en los proyectos nuevos desde Laravel 11. Aprovecha todas las aserciones de PHPUnit y añade una sintaxis concisa basada en closures.

Principales diferencias con PHPUnit:

| Aspecto            | Pest                                 | PHPUnit                  |
| ------------------ | ------------------------------------ | ------------------------ |
| Definición de test | Closures con `test()` / `it()`       | Clases con métodos       |
| Aserciones         | Expectation API (`expect()->toBe()`) | `$this->assert*()`       |
| Datasets           | `dataset()` / `with()`               | `@dataProvider`          |
| Hooks              | `beforeEach()` / `afterEach()`       | `setUp()` / `tearDown()` |
| Anidamiento        | Agrupación con `describe()`          | Separación por clases    |

<Info>
  Como los tests de Pest se ejecutan sobre PHPUnit, pueden convivir con tests PHPUnit existentes. Los ejecutas con `php artisan test` o `vendor/bin/pest`.
</Info>

## Cuándo usar `describe` / `it` / `test`

### `test()`

La definición más sencilla. Usa el nombre del test como su descripción.

```php theme={null}
test('el usuario puede iniciar sesión con email', function () {
    $user = User::factory()->create();

    $response = $this->post('/login', [
        'email' => $user->email,
        'password' => 'password',
    ]);

    $response->assertRedirect('/dashboard');
});
```

### `it()`

Permite un estilo cercano al lenguaje natural, tipo «it should...».

```php theme={null}
it('redirige a login a los usuarios no autenticados', function () {
    $response = $this->get('/dashboard');

    $response->assertRedirect('/login');
});
```

### `describe()`

Agrupa tests relacionados. Útil para compartir `beforeEach()` y organizar lógicamente los archivos.

```php theme={null}
describe('Gestión de pedidos', function () {
    beforeEach(function () {
        $this->user = User::factory()->create();
        $this->actingAs($this->user);
    });

    it('obtiene la lista de pedidos', function () {
        Order::factory(3)->for($this->user)->create();

        $response = $this->get('/orders');

        $response->assertOk()->assertJsonCount(3, 'data');
    });

    it('no obtiene pedidos de otros usuarios', function () {
        $other = User::factory()->create();
        Order::factory()->for($other)->create();

        $response = $this->get('/orders');

        $response->assertOk()->assertJsonCount(0, 'data');
    });
});
```

<Tip>
  `describe()` admite anidamiento, pero un anidamiento excesivo dificulta la lectura. Como regla general, no pases de dos niveles.
</Tip>

## Expectation API

`expect()` es la sintaxis de aserción propia de Pest. Permite encadenar condiciones.

### Aserciones básicas

```php theme={null}
expect($value)->toBe(42);               // Igualdad estricta (===)
expect($value)->toEqual(['a' => 1]);    // Igualdad laxa (==)
expect($value)->toBeTrue();
expect($value)->toBeFalse();
expect($value)->toBeNull();
expect($value)->not->toBeNull();

expect($string)->toContain('Laravel');
expect($array)->toHaveCount(3);
expect($array)->toHaveKey('email');
expect($array)->toContain('admin');

expect($number)->toBeGreaterThan(0);
expect($number)->toBeLessThanOrEqual(100);
```

### Aserciones sobre modelos

```php theme={null}
expect($user)->toBeInstanceOf(User::class);
expect($user->email)->toMatchRegex('/^.+@.+\..+$/');

// Verificar la persistencia en BD
expect(User::where('email', 'test@example.com')->exists())->toBeTrue();
```

### Cadena con `and()`

```php theme={null}
expect($response->status())->toBe(200)
    ->and($response->json('name'))->toBe('Laravel')
    ->and($response->json('version'))->toBeGreaterThan(12);
```

### Verificar cada elemento con `each()`

```php theme={null}
$users = User::factory(3)->create();

expect($users)->each(function ($user) {
    $user->toBeInstanceOf(User::class)
        ->email->not->toBeNull();
});
```

## Tests parametrizados con datasets

Para probar la misma lógica con varios valores, usa `dataset()` o `with()` en línea.

### Dataset en línea

```php theme={null}
it('los emails inválidos disparan error de validación', function (string $email) {
    $response = $this->post('/register', ['email' => $email]);

    $response->assertInvalid('email');
})->with([
    'texto plano' => ['not-an-email'],
    'sin dominio' => ['user@'],
    'cadena vacía' => [''],
]);
```

### Dataset con nombre

Define datasets en el directorio `tests/Datasets`.

```php theme={null}
// tests/Datasets/InvalidEmails.php
dataset('invalid_emails', [
    'plain' => ['not-an-email'],
    'no-domain' => ['user@'],
    'empty' => [''],
    'spaces' => ['user @example.com'],
]);
```

```php theme={null}
it('los emails inválidos disparan error de validación', function (string $email) {
    $response = $this->post('/register', ['email' => $email]);

    $response->assertInvalid('email');
})->with('invalid_emails');
```

### Dataset dinámico con closures

```php theme={null}
it('cada plan tiene un límite distinto', function (string $plan, int $limit) {
    $user = User::factory()->create(['plan' => $plan]);

    expect($user->requestLimit())->toBe($limit);
})->with([
    ['free', 100],
    ['pro', 1000],
    ['enterprise', 10000],
]);
```

## Tests unitarios con Mockery

### Mockear un servicio

```php theme={null}
use App\Services\PaymentGateway;
use Mockery\MockInterface;

test('se invoca el servicio de pagos', function () {
    $mock = $this->mock(PaymentGateway::class, function (MockInterface $mock) {
        $mock->expects('charge')
            ->with(1000, 'jpy')
            ->andReturn(['status' => 'succeeded']);
    });

    $result = app(PaymentGateway::class)->charge(1000, 'jpy');

    expect($result['status'])->toBe('succeeded');
});
```

### Spy

Un spy ejecuta la implementación real y registra las llamadas.

```php theme={null}
use App\Services\NotificationService;

test('se ha invocado el servicio de notificaciones', function () {
    $spy = $this->spy(NotificationService::class);

    $this->post('/orders', ['amount' => 1000]);

    $spy->shouldHaveReceived('send')->once()->with('order.created');
});
```

### Mock parcial

Se mockean solo algunos métodos y el resto se usa con la implementación real.

```php theme={null}
use App\Services\ReportService;
use Mockery\MockInterface;

test('salta la escritura de archivo del report', function () {
    $mock = $this->partialMock(ReportService::class, function (MockInterface $mock) {
        $mock->expects('writeFile')->andReturnNull();
    });

    $result = $mock->generate();

    expect($result)->not->toBeNull();
});
```

## Pruebas de llamadas a APIs externas con HTTP fake

Con `Http::fake()` puedes hacer stub de las respuestas sin enviar peticiones reales.

### Fake básico

```php theme={null}
use Illuminate\Support\Facades\Http;

test('obtiene información de usuario desde la API externa', function () {
    Http::fake([
        'https://api.example.com/users/*' => Http::response([
            'id' => 1,
            'name' => 'Laravel User',
        ], 200),
    ]);

    $result = app(UserApiClient::class)->find(1);

    expect($result['name'])->toBe('Laravel User');
    Http::assertSent(fn ($request) => $request->url() === 'https://api.example.com/users/1');
});
```

### Probar respuestas de error

```php theme={null}
test('lanza una excepción cuando la API devuelve error', function () {
    Http::fake([
        'api.example.com/*' => Http::response([], 503),
    ]);

    expect(fn () => app(UserApiClient::class)->find(1))
        ->toThrow(\App\Exceptions\ApiUnavailableException::class);
});
```

### Simular fallos de red

```php theme={null}
use Illuminate\Http\Client\ConnectionException;

test('gestiona los errores de conexión', function () {
    Http::fake(fn () => throw new ConnectionException('Connection refused'));

    $result = app(UserApiClient::class)->findWithFallback(1);

    expect($result)->toBeNull();
});
```

## Fakes de eventos, correos y notificaciones

### `Event::fake()`

```php theme={null}
use Illuminate\Support\Facades\Event;
use App\Events\OrderCreated;

test('se dispara el evento al crear un pedido', function () {
    Event::fake();

    $this->post('/orders', ['product_id' => 1, 'amount' => 1000]);

    Event::assertDispatched(OrderCreated::class, function ($event) {
        return $event->order->amount === 1000;
    });
});
```

También puedes falsear solo algunos eventos y dejar que los demás se procesen normalmente.

```php theme={null}
Event::fake([OrderCreated::class]);
```

### `Mail::fake()`

```php theme={null}
use Illuminate\Support\Facades\Mail;
use App\Mail\WelcomeEmail;

test('se envía el email de bienvenida al registrarse', function () {
    Mail::fake();

    $this->post('/register', [
        'name' => 'Test User',
        'email' => 'test@example.com',
        'password' => 'password',
        'password_confirmation' => 'password',
    ]);

    Mail::assertSent(WelcomeEmail::class, function ($mail) {
        return $mail->hasTo('test@example.com');
    });
});
```

### `Notification::fake()`

```php theme={null}
use Illuminate\Support\Facades\Notification;
use App\Notifications\OrderShipped;

test('se envía la notificación al hacer el envío', function () {
    Notification::fake();

    $user = User::factory()->create();
    $order = Order::factory()->for($user)->create();

    $this->post("/orders/{$order->id}/ship");

    Notification::assertSentTo($user, OrderShipped::class, function ($notification) use ($order) {
        return $notification->order->id === $order->id;
    });
});
```

## Pruebas de comandos Artisan

```php theme={null}
use Illuminate\Support\Facades\Artisan;

test('el comando de limpieza de datos funciona', function () {
    $expired = Order::factory()->create(['expires_at' => now()->subDay()]);
    $valid = Order::factory()->create(['expires_at' => now()->addDay()]);

    $this->artisan('orders:cleanup')
        ->assertSuccessful()
        ->expectsOutput('Cleaned up 1 expired order(s).');

    expect(Order::find($expired->id))->toBeNull();
    expect(Order::find($valid->id))->not->toBeNull();
});
```

### Comandos interactivos

```php theme={null}
test('un comando interactivo ejecuta el proceso tras la confirmación', function () {
    $this->artisan('reports:generate')
        ->expectsQuestion('¿Quieres ejecutarlo?', 'yes')
        ->expectsOutput('Report generado.')
        ->assertExitCode(0);
});
```

## `RefreshDatabase` y `LazilyRefreshDatabase`

### `RefreshDatabase`

Hace rollback de la BD tras cada test para mantenerla limpia. Ejecuta las migraciones al inicio del test suite.

```php theme={null}
// tests/Pest.php
uses(RefreshDatabase::class)->in('Feature');
```

```php theme={null}
use Illuminate\Foundation\Testing\RefreshDatabase;

test('permite crear un usuario', function () {
    $user = User::factory()->create(['name' => 'Laravel']);

    expect(User::count())->toBe(1);
    expect($user->name)->toBe('Laravel');
});
```

### `LazilyRefreshDatabase`

`RefreshDatabase` comprueba migraciones en todos los tests, pero `LazilyRefreshDatabase` las difiere hasta que un test toca de verdad la BD. Acelera los suites con muchos tests que no usan BD.

```php theme={null}
// tests/Pest.php
uses(LazilyRefreshDatabase::class)->in('Feature');
```

<Tip>
  Para la mayoría de proyectos, `RefreshDatabase` es la opción segura. Considera `LazilyRefreshDatabase` cuando el suite es grande y muchos tests no tocan la BD.
</Tip>

| Aspecto                                 | `RefreshDatabase`         | `LazilyRefreshDatabase`               |
| --------------------------------------- | ------------------------- | ------------------------------------- |
| Momento de ejecución de las migraciones | Al iniciar el test suite. | En la primera operación de BD.        |
| Sobrecoste en tests sin BD              | Sí.                       | No.                                   |
| Cuándo usarlo                           | Caso general.             | Suite grande con muchos tests sin BD. |

## Medición de cobertura de código

Requiere Xdebug o PCOV.

```bash theme={null}
# Mostrar la cobertura en la terminal
php artisan test --coverage

# Cobertura mínima obligatoria (CI falla si baja)
php artisan test --coverage --min=80

# Generar un informe HTML
vendor/bin/pest --coverage-html=coverage/

# Detectar tests lentos
php artisan test --profile
```

<Warning>
  La medición de cobertura aumenta mucho el tiempo de ejecución. En CI, es recomendable separarla en un job específico o ejecutarla solo en las pull requests.
</Warning>

## Páginas relacionadas

<Card title="Introducción a los tests" icon="flask" href="/es/testing">
  Consulta la forma estándar de escribir tests en Laravel y el uso de `php artisan test`.
</Card>


## Related topics

- [Gestión de la compatibilidad de versiones de paquetes](/es/advanced/package-versioning.md)
- [Pruebas de consola](/es/console-tests.md)
- [Introducción a las pruebas](/es/testing.md)
- [Despliegue](/es/deployment.md)
- [Pruebas de base de datos](/es/database-testing.md)
