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 --devDespué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/referenceY 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: emailAsí 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 contratoSi 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.
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.
star Incluido en cualquier suscripción