Saltar al contenido

Si mantienes una API, sabes lo que pasa. Empiezas con una especificación OpenAPI impecable, toda la documentación perfecta, y tres sprints después el endpoint /users devuelve campos que no existen en el spec, alguien añadió un parámetro nuevo que nadie documentó, y el frontend lleva dos días peleándose con una respuesta que no coincide con lo que dice la documentación.

Es un problema clásico: la documentación y el código se desacoplan. Y cuanto más grande es el equipo, más rápido ocurre.

Laravel Spectator resuelve exactamente esto. Es un paquete creado por Adam Campbell que se integra con tu suite de tests para verificar automáticamente que tus requests y responses cumplen con tu especificación OpenAPI. Si el código y la documentación no coinciden, el test falla.

Requisitos

Dependencia

Versión mínima

PHP

8.1+

Laravel

10, 11 o 12

Spectator

2.1.1

Pest

4.0 (recomendado)

Instalación

composer require hotmeteor/spectator --dev

Después publicamos la configuración:

php artisan vendor:publish --provider="Spectator\SpectatorServiceProvider"

Esto creará el archivo config/spectator.php donde configurarás el origen de tu especificación.

Configurar el origen de tu spec

Spectator soporta tres fuentes para tu archivo OpenAPI: local, remoto y GitHub. La más habitual es local. En tu .env añade:

SPEC_SOURCE=local
SPEC_PATH=/spec/reference

Y en config/spectator.php puedes ajustar las rutas para cada tipo de fuente:

<?php

return [
    'default' => env('SPEC_SOURCE', 'local'),

    'sources' => [
        'local' => [
            'source' => \Spectator\Sources\LocalSource::class,
            'base_path' => env('SPEC_PATH', 'spec'),
        ],

        'remote' => [
            'source' => \Spectator\Sources\RemoteSource::class,
            'base_url' => env('SPEC_URL'),
        ],

        'github' => [
            'source' => \Spectator\Sources\GithubSource::class,
            'base_url' => env('SPEC_GITHUB_URL'),
            'token' => env('SPEC_GITHUB_TOKEN'),
            'repo' => env('SPEC_GITHUB_REPO'),
        ],
    ],
];

Fíjate en que defines la carpeta donde vive el spec, no el archivo en sí. Esto te da flexibilidad para trabajar con múltiples APIs o con un spec dividido en varios archivos.

Tu primer test con Spectator

Imagina que tienes este spec OpenAPI para un endpoint de usuarios:

# spec/reference/Api.v1.yaml
openapi: "3.0.0"
info:
  title: Mi API
  version: "1.0"
paths:
  /api/users:
    get:
      summary: Listar usuarios
      responses:
        "200":
          description: Lista de usuarios
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  required:
                    - id
                    - name
                    - email
                  properties:
                    id:
                      type: integer
                    name:
                      type: string
                    email:
                      type: string
                      format: email
    post:
      summary: Crear usuario
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - email
              properties:
                name:
                  type: string
                email:
                  type: string
                  format: email
      responses:
        "201":
          description: Usuario creado
          content:
            application/json:
              schema:
                type: object
                required:
                  - id
                  - name
                  - email
                properties:
                  id:
                    type: integer
                  name:
                    type: string
                  email:
                    type: string
                    format: email

Así es como se ve un test funcional clásico en Laravel con Pest:

<?php

use App\Models\User;

it('lista usuarios', function () {
    User::factory()->count(3)->create();

    $this->getJson('/api/users')
        ->assertStatus(200)
        ->assertJsonCount(3);
});

Y así queda añadiendo Spectator:

<?php

use App\Models\User;
use Spectator\Spectator;

beforeEach(function () {
    Spectator::using('Api.v1.yaml');
});

it('lista usuarios', function () {
    User::factory()->count(3)->create();

    $this->getJson('/api/users')
        ->assertValidRequest()
        ->assertValidResponse(200);
});

it('crea un usuario', function () {
    $this->postJson('/api/users', [
        'name' => 'Isra',
        'email' => '[email protected]',
    ])
        ->assertValidRequest()
        ->assertValidResponse(201);
});

La diferencia clave son dos métodos: assertValidRequest() y assertValidResponse(). Debajo del capó, Spectator registra un middleware que compara lo que envías y lo que recibes con lo que dice tu especificación OpenAPI. Si no coincide, el test falla.

Qué valida exactamente Spectator

Es importante entender el alcance. Spectator hace contract testing, no testing funcional:

assertValidRequest() verifica que los parámetros, headers y body del request coinciden con lo que el spec dice que ese endpoint acepta. assertValidResponse($statusCode) verifica que el body de la respuesta coincide con el schema definido para ese código de estado.

Lo que Spectator no hace es verificar que los datos sean correctos desde el punto de vista de negocio. Puede pasar que tu endpoint devuelva datos totalmente equivocados, pero si la estructura coincide con el spec, el contract test pasa. Para eso sigues necesitando tus tests funcionales habituales.

La buena noticia es que puedes combinar ambos sin problema:

<?php

use App\Models\User;
use Spectator\Spectator;

beforeEach(function () {
    Spectator::using('Api.v1.yaml');
});

it('crea un usuario correctamente', function () {
    $this->postJson('/api/users', [
        'name' => 'Isra',
        'email' => '[email protected]',
    ])
        // Contract testing: ¿cumple con el spec?
        ->assertValidRequest()
        ->assertValidResponse(201)
        // Functional testing: ¿hace lo que debe?
        ->assertJsonFragment([
            'name' => 'Isra',
            'email' => '[email protected]',
        ]);

    $this->assertDatabaseHas('users', [
        'email' => '[email protected]',
    ]);
});

Trabajar con múltiples specs

Si tu proyecto maneja varias APIs o versiones, puedes cambiar de spec dinámicamente en cada test:

<?php

use Spectator\Spectator;

it('funciona con v1', function () {
    Spectator::using('Api.v1.yaml');

    $this->getJson('/api/v1/users')
        ->assertValidRequest()
        ->assertValidResponse(200);
});

it('funciona con v2', function () {
    Spectator::using('Api.v2.yaml');

    $this->getJson('/api/v2/users')
        ->assertValidRequest()
        ->assertValidResponse(200);
});

Y si necesitas desactivar Spectator para un test concreto dentro de un archivo que lo usa globalmente:

<?php

use Spectator\Spectator;

beforeEach(function () {
    Spectator::using('Api.v1.yaml');
});

it('funciona sin spectator', function () {
    Spectator::reset();

    // Este test se ejecuta sin validación de contrato
    $this->getJson('/api/health')
        ->assertStatus(200);
});

Depuración: cuando algo falla

Por defecto, Spectator formatea los errores de forma concisa. Si necesitas más detalle para depurar, desactiva el manejo de excepciones de Laravel:

<?php

use Spectator\Spectator;

beforeEach(function () {
    Spectator::using('Api.v1.yaml');
});

it('debug del endpoint', function () {
    $this->withoutExceptionHandling();

    $this->getJson('/api/users')
        ->assertValidRequest()
        ->assertValidResponse(200);
});

Enfoque TDD con Spectator

Spectator encaja perfectamente con TDD. El flujo sería: primero defines el contrato en tu spec OpenAPI, después escribes el test que referencia ese spec, y por último implementas el endpoint hasta que el test pase.

<?php

use Spectator\Spectator;

beforeEach(function () {
    Spectator::using('Api.v1.yaml');
});

// 1. Escribes el test primero (rojo)
it('lista productos', function () {
    $this->getJson('/api/products')
        ->assertValidRequest()
        ->assertValidResponse(200);
});

// 2. Implementas la ruta y el controlador (verde)
// 3. Refactorizas manteniendo el contrato

Si en algún momento cambias la respuesta del controlador y dejas de cumplir el spec, el test falla. Si actualizas el spec pero no el código, también falla. Ambos deben ir siempre sincronizados.

Cuándo usar Spectator (y cuándo no)

Spectator es especialmente útil cuando tu API es consumida por equipos externos (frontend, móvil, terceros), cuando trabajas con un spec OpenAPI como contrato entre equipos, cuando quieres automatizar la detección de breaking changes en tu API, o cuando mantienes documentación pública de tu API y necesitas que sea fiable.

No necesitas Spectator si tu API es interna y no mantienes un spec OpenAPI, si estás en fases muy tempranas donde el spec cambia constantemente, o si prefieres herramientas como Scramble que generan el spec automáticamente desde el código (enfoque opuesto pero igualmente válido).

Conclusión

Spectator no reemplaza tus tests funcionales. Los complementa añadiendo una capa de validación que garantiza que tu documentación OpenAPI y tu código real están sincronizados. Con dos assertions (assertValidRequest y assertValidResponse) integras contract testing en tu suite de tests existente sin fricción.

Si mantienes APIs que otros equipos consumen, es uno de esos paquetes que se paga solo desde el primer día.

school Curso completo

Curso Laravel 12
Completo 2026

El único curso 100% actualizado que incluye Laravel 12, Livewire 3, Vue 3, React 19 e Inertia 2. Aprende con proyectos reales y las últimas funcionalidades.

access_time 8 horas de contenido
layers 4 tecnologías en 1
update 100% actualizado
code Proyectos prácticos
Ver Curso Laravel 12 arrow_forward

star Incluido en cualquier suscripción

Rutas de aprendizaje