CommunityEscrita e Ediçãogithub.com

shaxzodbek-uzb/laravel-guardrails

🛡️ Habilidades de guardrails de produção para Laravel assistido por IA — evite vazamentos entre inquilinos, N+1, migrações destrutivas antes que seu agente de IA as envie. Para Claude Code, Cursor e mais de 40 agentes.

O que é laravel-guardrails?

Esta habilidade estabelece uma disciplina rigorosa de testes com Pest 3/4 para projetos Laravel, garantindo que agentes de IA escrevam testes que provam comportamento, cobrem caminhos de erro e autorização, e sobrevivem a refatorações. Ela previne dois modos de falha comuns: ausência de testes ou testes quebradiços com mocks excessivos. As regras cobrem desde o uso correto de fábricas e datasets até a aplicação de testes de arquitetura como barreiras globais de baixo custo. A habilidade é compatível com Claude Code, Cursor e mais de 40 agentes de IA, fornecendo um guia prático com exemplos de código bons e ruins, além de comandos para verificar cobertura, vazamentos de autorização e cheiros de over-mocking.

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

Installed? Explore more Escrita e Edição skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Disciplina de Testes com Pest

Funcionalidades enviadas sem teste — ou "testadas" com suítes excessivamente simuladas que afirmam detalhes de implementação — quebram silenciosamente em produção e apodrecem a cada refatoração. Esta habilidade faz você escrever testes Pest 3/4 que provam comportamento, cobrem o caminho infeliz e autorização, e sobrevivem a refatorações.

A armadilha

Dois modos de falha, ambos caros:

  1. Sem testes / testes apenas do caminho feliz. A funcionalidade funciona na demonstração, então em produção um convidado acessa o endpoint, um inquilino lê dados de outro inquilino, uma lacuna de validação deixa entrar lixo, ou um 404 vira um 500. Falhas de autorização (caminhos 403/404 que nunca foram afirmados) são como vazamentos de dados chegam à produção.
  2. Testes quebradiços e excessivamente simulados. Você simula seu próprio UserRepository/serviço com shouldReceive('save')->once(), então o teste passa mesmo quando a consulta real está errada, e explode no momento em que alguém renomeia um método. A suíte está verde, dá falsa confiança, e é deletada por frustração durante a próxima refatoração — deixando você de volta ao modo 1.

A correção é a mesma disciplina em ambos os casos: finja os limites que você não possui, exercite o código que você possui, e afirme resultados observáveis (linhas do banco de dados, status HTTP, jobs/mails despachados) — incluindo os resultados de falha e autorização.

Regras

  1. Escreva o teste com a funcionalidade, não "depois". Uma alteração em controller/job/command/policy está incompleta até que tenha um teste Pest. Nunca relate uma funcionalidade como concluída sem um teste correspondente que afirme seu comportamento.
  2. Use expressões idiomáticas do Pest. Defina casos com it('...') ou test('...'); afirme com o fluente expect(...); compartilhe configuração com beforeEach(). Não escreva PHPUnit class FooTest extends TestCase a menos que o arquivo já use esse estilo.
  3. Escolha o tipo certo. Comportamento HTTP/ponta a ponta → Teste de Funcionalidade (inicializa o framework, acessa rotas via $this->get/post/...). Lógica pura sem framework/BD → Teste Unitário. Padrão: Teste de Funcionalidade quando em dúvida — ele pega mais.
  4. Redefina o banco de dados com uma trait, via uses(). Aplique RefreshDatabase (envolvido em transação) ou LazilyRefreshDatabase (migra apenas quando um teste toca o BD) em tests/Pest.php para todo o diretório Feature, ou uses(RefreshDatabase::class) no topo de um arquivo. NUNCA deixe testes de BD dependentes de estado residual.
  5. Construa dados com fábricas, nunca inserções manuais. Use User::factory()->create() / ->make() e fábricas de relacionamentos. Construções manuais DB::table()->insert([...]) ou new Model([...])->save() em testes são frágeis e pulam casts/padrões — proíba isso.
  6. Desduplique com datasets, não copie e cole. Quando a mesma afirmação é executada sobre muitas entradas (cargas válidas/inválidas, matrizes de papéis), use ->with([...]) (inline ou um dataset nomeado em tests/Datasets/) em vez de N testes quase idênticos.
  7. Finja os limites que você não possui. Para qualquer coisa externa ou assíncrona, use os fakes das fachadas antes da ação: Http::fake(), Queue::fake(), Bus::fake(), Mail::fake(), Notification::fake(), Event::fake(), Storage::fake(). Para tempo, use $this->travel(...), $this->travelTo(...), ou $this->freezeTime() — nunca afirme contra um now() não congelado.
  8. NÃO simule seus próprios modelos/serviços até perder o sentido. Evite Mockery::mock(MyService::class)->shouldReceive(...) e Model::shouldReceive(...) para código sob teste. Deixe o código real rodar contra o BD de teste e os limites falsificados. Simule sua própria classe apenas em uma costura genuína que você não está exercitando (por exemplo, um wrapper lento de SDK de terceiros).
  9. Afirme resultados, não detalhes internos. Prefira assertDatabaseHas / assertDatabaseMissing / assertModelExists, assertStatus / assertOk / assertCreated, assertJson / assertJsonPath, assertRedirect, Mail::assertSent, Notification::assertSentTo, Queue::assertPushed, Bus::assertDispatched, Storage::disk()->assertExists. Não afirme "método X foi chamado uma vez" como proxy para comportamento.
  10. Sempre teste o caminho infeliz E a autorização. Cada endpoint/ação precisa, no mínimo: o caminho feliz, um caminho de validação/falha, e os caminhos de autenticação — convidado → 401/redirecionamento, usuário errado/papel → 403, recurso ausente/estrangeiro → 404. Para qualquer funcionalidade sensível à segurança, adicione um teste explícito de "outro usuário não pode acessar/modificar isso". Isso é inegociável para qualquer coisa que toque em propriedade ou inquilinato.
  11. Adicione testes de arquitetura como guardrails globais baratos. Mantenha um tests/Arch.php (ou casos arch em Pest.php) com os presets que se encaixam: arch()->preset()->php() (proíbe saída de debug die/var_dump/dd e funções PHP obsoletas), arch()->preset()->security() (proíbe eval, extract, unserialize, hashing fraco/md5/sha1, aleatoriedade insegura, etc.), e arch()->preset()->laravel() (impõe convenções do Laravel — incluindo nenhum env() fora da config). Adicione invariantes do projeto como arch('controllers')->expect('App\Http\Controllers')->toExtend('App\Http\Controllers\Controller').
  12. Um comportamento por teste, nome descritivo. it('bloqueia convidados do painel'), não it('funciona'). Se um nome de teste precisar de "e", divida-o.
  13. Portão de cobertura no CI. Execute ./vendor/bin/pest --coverage --min=NN para impor um mínimo, e ./vendor/bin/pest --type-coverage (requer o plugin type-coverage) para impor assinaturas tipadas. Trate novas linhas não testadas como um defeito.
  14. Browser/E2E com moderação (Pest 4). O Pest 4 oferece testes de navegador via visit('/')->...->assertSee(...) (baseado em Playwright). Reserve-o para fluxos full-stack/JS verdadeiros; não reimplemente asserções de camada HTTP como testes de navegador lentos.

Bom vs ruim

// tests/Feature/UpdatePostTest.php

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

// ❌ excessivamente simulado: testa que um método foi chamado, não o que aconteceu.
//    Passa mesmo se o post errado for atualizado ou a autenticação estiver faltando.
it('atualiza um 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' => 'Novo'])->assertOk();
});
// ✅ exercita código real contra o BD de teste; afirma o resultado E a autorização.
use App\Models\Post;
use App\Models\User;

it('permite que o proprietário atualize seu post', function () {
    $owner = User::factory()->create();
    $post  = Post::factory()->for($owner)->create(['title' => 'Antigo']);

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

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

it('proíbe um não proprietário de atualizar o post', function () {
    $post     = Post::factory()->create(['title' => 'Antigo']);
    $attacker = User::factory()->create();

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

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

it('bloqueia convidados', function () {
    $post = Post::factory()->create();

    $this->put("/posts/{$post->id}", ['title' => 'Novo'])
        ->assertRedirect('/login'); // ou assertUnauthorized() para uma API
});
// ❌ acessa a rede real (instável, lento, pode vazar chaves); não afirma nada útil.
it('notifica o slack', function () {
    $resp = (new App\Services\Slack)->ping('deploy ok');
    expect($resp)->toBeArray();
});
// ✅ finja o limite que você não possui; afirme a requisição que você enviou.
use Illuminate\Support\Facades\Http;

it('envia a mensagem de deploy para o slack', function () {
    Http::fake(['hooks.slack.com/*' => Http::response(['ok' => true])]);

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

    Http::assertSent(fn ($request) =>
        $request->url() === 'https://hooks.slack.com/services/T/B/x'
        && $request['text'] === 'deploy ok'
    );
});
// ❌ testes de validação quase idênticos copiados e colados.
it('rejeita email vazio', function () {
    $this->post('/register', ['email' => ''])->assertSessionHasErrors('email');
});
it('rejeita email ruim', function () {
    $this->post('/register', ['email' => 'nope'])->assertSessionHasErrors('email');
});
// ✅ um teste, orientado por tabela com um dataset.
it('rejeita emails inválidos', function (string $email) {
    $this->post('/register', ['email' => $email])
        ->assertSessionHasErrors('email');
})->with([
    'vazio'       => '',
    'sem arroba'  => 'nope',
    'sem domínio' => 'a@',
]);
// ✅ limites assíncronos + de tempo falsificados; afirme o despacho e o efeito colateral.
use App\Jobs\SendReminder;
use App\Mail\WelcomeMail;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Queue;

it('enfileira um lembrete e dá boas-vindas ao usuário no cadastro', function () {
    Queue::fake();
    Mail::fake();
    $this->freezeTime();

    $this->post('/register', [
        'name' => 'Ada', 'email' => '[email protected]', 'password' => 'senha-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 — ✅ guardrails globais baratos
arch()->preset()->php();        // proíbe saída de debug die/var_dump/dd + funções PHP obsoletas
arch()->preset()->security();   // proíbe eval, extract, unserialize, md5/sha1, aleatoriedade fraca, etc.
arch()->preset()->laravel();    // impõe convenções do Laravel, incl. nenhum env() fora da config

arch('controllers estendem o controller base')
    ->expect('App\Http\Controllers')
    ->toExtend('App\Http\Controllers\Controller');

arch('models vivem no namespace certo')
    ->expect('App\Models')
    ->toExtend('Illuminate\Database\Eloquent\Model');

Como verificar

Execute estes comandos antes de declarar uma funcionalidade + seus testes como concluídos:

# 1. A suíte inteira deve estar verde.
./vendor/bin/pest

# 2. Execute apenas o(s) arquivo(s) novo(s)/alterado(s) enquanto itera.
./vendor/bin/pest tests/Feature/UpdatePostTest.php

# 3. Imponha um piso de cobertura (requer Xdebug ou PCOV).
./vendor/bin/pest --coverage --min=80

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

# 5. Estilo permanece limpo.
./vendor/bin/pint --test

Em seguida, verifique se você NÃO reintroduziu a armadilha:

# Cobertura de autorização: cada teste de funcionalidade protegida deve afirmar um caminho 401/403/404.
grep -rEn "assertForbidden|assertUnauthorized|assertNotFound|->assertStatus\\((401|403|404)\\)" tests/

# Cheiro de over-mocking: simular suas PRÓPRIAS classes de aplicação é uma bandeira vermelha — revise cada ocorrência.
grep -rEn "Mockery::|->shouldReceive\(|::partialMock\(|::spy\\(" tests/

# Inserções manuais em vez de fábricas — substitua por ->factory().
grep -rEn "DB::table\(.*\)->insert|new App\\\\Models" tests/

# Limites externos devem ser falsificados — confirme que fakes existem onde o código chama algo externo.
grep -rEn "Http::fake|Queue::fake|Bus::fake|Mail::fake|Notification::fake|Storage::fake|Event::fake" tests/

# Os presets de arquitetura security + php (os guardrails globais baratos) devem estar presentes em algum lugar.
grep -rEn "preset\(\)->(security|php|laravel)\(\)" tests/

Afirme em testes, não apenas em código: para cada novo endpoint, confirme que há (a) uma afirmação de resultado do caminho feliz (assertDatabaseHas/assertOk/assertJsonPath), (b) pelo menos uma afirmação de falha/validação, e (c) as afirmações de autorização de convidado + usuário errado. Se algum faltar, a funcionalidade não está concluída.

Quando é aceitável flexibilizar a regra

  • Simular seu próprio código em uma costura externa genuína. Um wrapper fino em torno de um SDK pago de terceiros (SMS, gateway de pagamento) que você não pode falsificar na camada HTTP é um alvo legítimo de Mockery — você está isolando a dependência deles, não falsificando o comportamento sob teste.
  • Testar unitariamente lógica pura sem o BD. Objetos de valor, formatadores e calculadoras não precisam de RefreshDatabase nem de fábricas — instancie-os diretamente e use expect() para o resultado; isso é mais rápido e correto.
  • make() em vez de create() quando um teste nunca persiste — User::factory()->make() evita uma gravação no BD para asserções de lógica pura.
  • Limiares de cobertura mais baixos em código legado. Defina um --min realista e aumente-o gradualmente em vez de bloquear todo o trabalho; nunca deixe "100% ou nada" se tornar uma desculpa para enviar zero testes.
  • Testes de navegador para fluxos genuinamente orientados a JS (interações Livewire/Inertia/Vue, seletores de arquivos) — aí, visit() é a ferramenta certa, não um exagero.

Referências

Habilidades Relacionadas