CommunityRedacción y edicióngithub.com

shaxzodbek-uzb/laravel-guardrails

🛡️ Habilidades de barandilla de producción para Laravel asistido por IA — evita fugas entre inquilinos, N+1, migraciones destructivas antes de que tu agente de IA las envíe. Para Claude Code, Cursor y más de 40 agentes.

¿Qué es laravel-guardrails?

Esta habilidad impone una disciplina de pruebas rigurosa para proyectos Laravel asistidos por IA, evitando dos modos de fallo comunes: pruebas inexistentes o solo del camino feliz (que generan fugas de datos, errores de autorización y roturas en producción), y pruebas frágiles con exceso de mocks (que dan falsa confianza y se rompen en cada refactorización). La solución es escribir pruebas Pest 3/4 que prueben *comportamiento* observable (filas en la base de datos, respuestas HTTP, trabajos enviados) en lugar de detalles de implementación, y que cubran tanto los caminos de error como la autorización. La habilidad proporciona 14 reglas prácticas, ejemplos concretos de código correcto e incorrecto, y un proceso de verificación automatizado con comandos para ejecutar la suite, comprobar cobertura, estilo y detectar malos olores como mocking propio o inserciones manuales. Está diseñada para funcionar con Claude Code, Cursor y más de 40 agentes de IA, y es compatible con cualquier proyecto Laravel que use Pest como marco de pruebas.

Compatible con✓Claude Code~Codex CLI✓Cursor
npx skills add shaxzodbek-uzb/laravel-guardrails

Installed? Explore more Redacción y edición skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Disciplina de Pruebas Pest

Las funcionalidades enviadas sin pruebas — o "probadas" con suites sobrecargadas de mocks que verifican detalles de implementación — se rompen silenciosamente en producción y se pudren en cada refactorización. Esta habilidad te hace escribir pruebas Pest 3/4 que prueban comportamiento, cubren el camino infeliz y la autorización, y sobreviven a refactorizaciones.

El peligro

Dos modos de fallo, ambos costosos:

  1. Sin pruebas / solo pruebas del camino feliz. La funcionalidad funciona en la demo, pero luego en producción un invitado accede al endpoint, un inquilino lee datos de otro inquilino, una brecha de validación permite la entrada de basura, o un 404 se convierte en 500. Los agujeros de autorización (caminos 403/404 que nunca se afirmaron) son cómo las fugas de datos llegan a producción.
  2. Pruebas frágiles y sobrecargadas de mocks. Mockeas tu propio UserRepository/servicio con shouldReceive('save')->once(), por lo que la prueba pasa incluso cuando la consulta real es incorrecta, y explota en cuanto alguien renombra un método. La suite está verde, da falsa confianza, y se elimina por frustración durante la próxima refactorización, dejándote de vuelta en el modo 1.

La solución es la misma disciplina en ambos casos: falsea los límites que no posees, ejercita el código que sí posees, y verifica resultados observables (filas de BD, estado HTTP, trabajos/correos enviados) — incluyendo los resultados de fallo y autorización.

Reglas

  1. Escribe la prueba con la funcionalidad, no "después". Un cambio en un controlador/trabajo/comando/regla de autorización está incompleto hasta que tenga una prueba Pest. Nunca reportes una funcionalidad como terminada sin una prueba correspondiente que verifique su comportamiento.
  2. Usa modismos de Pest. Define casos con it('...') o test('...'); verifica con expect(...) fluido; comparte configuración con beforeEach(). No escribas class FooTest extends TestCase de PHPUnit a menos que el archivo ya use ese estilo.
  3. Elige el tipo correcto. Comportamiento HTTP/de extremo a extremo → Prueba de característica (inicia el framework, accede a rutas mediante $this->get/post/...). Lógica pura sin framework/BD → Prueba unitaria. Por defecto, elige una prueba de característica si tienes dudas: atrapa más.
  4. Resetea la base de datos con un trait, mediante uses(). Aplica RefreshDatabase (envuelto en transacciones) o LazilyRefreshDatabase (solo migra cuando una prueba toca la BD) en tests/Pest.php para todo el directorio Feature, o uses(RefreshDatabase::class) al inicio de un archivo. NUNCA dejes que las pruebas de BD dependan de estado residual.
  5. Construye datos con factories, nunca inserciones hechas a mano. Usa User::factory()->create() / ->make() y factories de relaciones. Las inserciones hechas a mano con DB::table()->insert([...]) o new Model([...])->save() en pruebas son frágiles y omiten conversiones/valores predeterminados — prohíbelas.
  6. Desduplica con conjuntos de datos, no copiando y pegando. Cuando la misma verificación se ejecuta sobre muchas entradas (cargas válidas/inválidas, matrices de roles), usa ->with([...]) (en línea o un conjunto de datos con nombre en tests/Datasets/) en lugar de N pruebas casi idénticas.
  7. Falsea los límites que no posees. Para cualquier cosa externa o asíncrona, usa los falsos de las fachadas antes de la acción: Http::fake(), Queue::fake(), Bus::fake(), Mail::fake(), Notification::fake(), Event::fake(), Storage::fake(). Para el tiempo, usa $this->travel(...), $this->travelTo(...), o $this->freezeTime() — nunca verifiques contra un now() no congelado.
  8. NO mockees tus propios modelos/servicios hasta hacerlos insignificantes. Evita Mockery::mock(MyService::class)->shouldReceive(...) y Model::shouldReceive(...) para el código bajo prueba. Deja que el código real se ejecute contra la BD de prueba y los límites falseados. Mockea tu propia clase solo en una costura genuina que no estés ejercitando (por ejemplo, un wrapper lento de SDK de terceros).
  9. Verifica resultados, no detalles internos. Prefiere assertDatabaseHas / assertDatabaseMissing / assertModelExists, assertStatus / assertOk / assertCreated, assertJson / assertJsonPath, assertRedirect, Mail::assertSent, Notification::assertSentTo, Queue::assertPushed, Bus::assertDispatched, Storage::disk()->assertExists. No verifiques "el método X se llamó una vez" como proxy del comportamiento.
  10. Prueba siempre el camino infeliz Y la autorización. Cada endpoint/acción necesita, como mínimo: el camino feliz, un camino de validación/fallo, y los caminos de autorización — invitado → 401/redirección, usuario/rol incorrecto → 403, recurso faltante/ajeno → 404. Para cualquier funcionalidad sensible a la seguridad, añade una prueba explícita de "otro usuario no puede acceder/modificar esto". Esto no es negociable para cualquier cosa que toque propiedad o inquilinato.
  11. Añade pruebas de arquitectura como barandillas globales económicas. Mantén un tests/Arch.php (o casos de arquitectura en Pest.php) con los preajustes que correspondan: arch()->preset()->php() (prohíbe la salida de depuración tipo die/var_dump/dd y funciones PHP obsoletas), arch()->preset()->security() (prohíbe eval, extract, unserialize, hash débiles/md5/sha1, aleatoriedad insegura, etc.), y arch()->preset()->laravel() (aplica las convenciones de Laravel — incluyendo no usar env() fuera de config). Añade invariantes del proyecto como arch('controllers')->expect('App\Http\Controllers')->toExtend('App\Http\Controllers\Controller').
  12. Un comportamiento por prueba, nombre descriptivo. it('bloquea a los invitados del panel'), no it('funciona'). Si un nombre de prueba necesita "y", divídelo.
  13. Controla la cobertura en CI. Ejecuta ./vendor/bin/pest --coverage --min=NN para forzar un mínimo, y ./vendor/bin/pest --type-coverage (requiere el plugin type-coverage) para forzar firmas tipadas. Trata las nuevas líneas no probadas como un defecto.
  14. Pruebas de navegador/E2E con moderación (Pest 4). Pest 4 incluye pruebas de navegador mediante visit('/')->...->assertSee(...) (basado en Playwright). Resérvalas para flujos full-stack/JS reales; no reimplementes verificaciones a nivel HTTP como pruebas de navegador lentas.

Bueno vs malo

// tests/Feature/UpdatePostTest.php

use App\Models\Post;
use App\Models\User;

// ❌ sobrecargado de mocks: prueba que se llamó un método, no lo que pasó.
//    Pasa incluso si se actualiza el post incorrecto o falta autorización.
it('actualiza un post', function () {
    $service = Mockery::mock(App\Services\PostService::class);
    $service->shouldReceive('update')->once()->andReturnTrue();
    $this->app->instance(App\Services\PostService::class, $service);

    $this->put('/posts/1', ['title' => 'Nuevo'])->assertOk();
});
// ✅ ejercita código real contra la BD de prueba; verifica el resultado Y la autorización.
use App\Models\Post;
use App\Models\User;

it('permite al propietario actualizar su post', function () {
    $owner = User::factory()->create();
    $post  = Post::factory()->for($owner)->create(['title' => 'Viejo']);

    $this->actingAs($owner)
        ->put("/posts/{$post->id}", ['title' => 'Nuevo'])
        ->assertRedirect();

    $this->assertDatabaseHas('posts', ['id' => $post->id, 'title' => 'Nuevo']);
});

it('prohíbe a un no-propietario actualizar el post', function () {
    $post     = Post::factory()->create(['title' => 'Viejo']);
    $attacker = User::factory()->create();

    $this->actingAs($attacker)
        ->put("/posts/{$post->id}", ['title' => 'Hackeado'])
        ->assertForbidden(); // 403

    $this->assertDatabaseHas('posts', ['id' => $post->id, 'title' => 'Viejo']);
});

it('bloquea a los invitados', function () {
    $post = Post::factory()->create();

    $this->put("/posts/{$post->id}", ['title' => 'Nuevo'])
        ->assertRedirect('/login'); // o assertUnauthorized() para una API
});
// ❌ accede a la red real (inestable, lento, puede filtrar claves); no verifica nada útil.
it('notifica a slack', function () {
    $resp = (new App\Services\Slack)->ping('despliegue ok');
    expect($resp)->toBeArray();
});
// ✅ falsea el límite que no posees; verifica la solicitud que enviaste.
use Illuminate\Support\Facades\Http;

it('envía el mensaje de despliegue a slack', function () {
    Http::fake(['hooks.slack.com/*' => Http::response(['ok' => true])]);

    (new App\Services\Slack)->ping('despliegue ok');

    Http::assertSent(fn ($request) =>
        $request->url() === 'https://hooks.slack.com/services/T/B/x'
        && $request['text'] === 'despliegue ok'
    );
});
// ❌ pruebas de validación casi idénticas copiadas y pegadas.
it('rechaza email vacío', function () {
    $this->post('/register', ['email' => ''])->assertSessionHasErrors('email');
});
it('rechaza email malo', function () {
    $this->post('/register', ['email' => 'nope'])->assertSessionHasErrors('email');
});
// ✅ una prueba, guiada por tabla con un conjunto de datos.
it('rechaza emails inválidos', function (string $email) {
    $this->post('/register', ['email' => $email])
        ->assertSessionHasErrors('email');
})->with([
    'vacío'       => '',
    'sin arroba'  => 'nope',
    'sin dominio' => 'a@',
]);
// ✅ límites asíncronos + tiempo falseados; verifica el envío y el efecto secundario.
use App\Jobs\SendReminder;
use App\Mail\WelcomeMail;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Queue;

it('pone en cola un recordatorio y da la bienvenida al usuario al registrarse', function () {
    Queue::fake();
    Mail::fake();
    $this->freezeTime();

    $this->post('/register', [
        'name' => 'Ada', 'email' => '[email protected]', 'password' => 'contraseña-secreta',
    ])->assertRedirect();

    Queue::assertPushed(SendReminder::class);
    Mail::assertSent(WelcomeMail::class, fn ($m) => $m->hasTo('[email protected]'));
    $this->assertDatabaseHas('users', ['email' => '[email protected]']);
});
// tests/Arch.php — ✅ barandillas globales económicas
arch()->preset()->php();        // prohíbe die/var_dump/dd y funciones PHP obsoletas
arch()->preset()->security();   // prohíbe eval, extract, unserialize, md5/sha1, aleatoriedad débil, etc.
arch()->preset()->laravel();    // aplica convenciones de Laravel, incl. no env() fuera de config

arch('los controladores extienden el controlador base')
    ->expect('App\Http\Controllers')
    ->toExtend('App\Http\Controllers\Controller');

arch('los modelos viven en el namespace correcto')
    ->expect('App\Models')
    ->toExtend('Illuminate\Database\Eloquent\Model');

Cómo verificar

Ejecuta esto antes de afirmar que una funcionalidad y sus pruebas están terminadas:

# 1. Toda la suite debe estar en verde.
./vendor/bin/pest

# 2. Ejecuta solo el/los archivo(s) nuevo(s) o modificado(s) mientras iteras.
./vendor/bin/pest tests/Feature/UpdatePostTest.php

# 3. Exige un mínimo de cobertura (requiere Xdebug o PCOV).
./vendor/bin/pest --coverage --min=80

# 4. Cobertura de tipos (requiere pestphp/pest-plugin-type-coverage).
./vendor/bin/pest --type-coverage --min=100

# 5. El estilo se mantiene limpio.
./vendor/bin/pint --test

Luego audita que NO reintrodujiste el peligro:

# Cobertura de autorización: cada prueba de funcionalidad protegida debe verificar un camino 401/403/404.
grep -rEn "assertForbidden|assertUnauthorized|assertNotFound|->assertStatus\\((401|403|404)\\)" tests/

# Olor a sobrecarga de mocks: hacer mock de tus PROPIAS clases de la app es una señal de alerta — revisa cada coincidencia.
grep -rEn "Mockery::|->shouldReceive\\(|::partialMock\\(|::spy\\(" tests/

# Inserciones hechas a mano en lugar de factories — reemplázalas con ->factory().
grep -rEn "DB::table\\(.*\\)->insert|new App\\\\Models" tests/

# Los límites externos deben falsearse — confirma que existen falsos donde el código hace llamadas externas.
grep -rEn "Http::fake|Queue::fake|Bus::fake|Mail::fake|Notification::fake|Storage::fake|Event::fake" tests/

# Los preajustes de arquitectura de seguridad + php (las barandillas globales económicas) deben estar presentes en algún lugar.
grep -rEn "preset\\(\\)->(security|php|laravel)\\(\\)" tests/

Verifica en pruebas, no solo en código: para cada nuevo endpoint, confirma que hay (a) una verificación de resultado del camino feliz (assertDatabaseHas/assertOk/assertJsonPath), (b) al menos una verificación de fallo/validación, y (c) las verificaciones de autorización de invitado + usuario incorrecto. Si falta alguna, la funcionalidad no está terminada.

Cuándo está bien doblar la regla

  • Mockear tu propio código en una costura externa real. Un wrapper delgado alrededor de un SDK de terceros de pago (SMS, pasarela de pago) que no puedes falsear a nivel HTTP es un objetivo legítimo de Mockery — estás aislando su dependencia, no falseando el comportamiento bajo prueba.
  • Probar lógica pura sin la BD. Objetos de valor, formateadores y calculadoras no necesitan RefreshDatabase ni factories — instáncialos directamente y usa expect() para el resultado; es más rápido y correcto.
  • make() en lugar de create() cuando una prueba nunca persiste — User::factory()->make() evita una escritura en BD para verificaciones de lógica pura.
  • Umbrales de cobertura más bajos en código heredado. Establece un --min realista y auméntalo con el tiempo en lugar de bloquear todo el trabajo; nunca dejes que "100% o nada" se convierta en una excusa para no enviar ninguna prueba.
  • Pruebas de navegador para flujos genuinamente impulsados por JS (interacciones de Livewire/Inertia/Vue, selectores de archivos) — allí, visit() es la herramienta correcta, no un exceso.

Referencias

Skills relacionados