agentsclimarketplace

Testing

Skill aronpc/ai/skills/testing

Cria testes com Pest PHP incluindo Feature, Unit, HTTP e Datasets. Use quando precisar escrever testes, criar test suites, ou implementar TDD em projetos Laravel com Pest.From its SKILL.md

Install
npx -y skills add aronpc/ai --skill testing

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

12.9 KB, as published. Nobody here has run it

testing

Resumo

Cria e mantém testes com Pest PHP para Feature, Unit, HTTP, Actions e Policies.

Skills Relacionadas

SkillQuando usar junto
qaPara validação completa de qualidade
coderPara criar testes durante implementação
architecturePara testar Actions e Policies
modelsPara criar Factories e testar Models
cicdPara pipelines de teste
pr-reviewPara verificar cobertura em PRs

Quando usar

Use esta skill sempre que:

  • Escrever testes para novas features
  • Aumentar cobertura de testes
  • Refatorar testes existentes
  • Criar mocks customizados
  • Testar Actions, Policies, Models
  • Testar endpoints HTTP
  • Criar datasets para testes
  • Configurar testes de multi-tenancy

Instalação e Configuração

Instalar Pest

composer require pestphp/pest --dev --with-all-dependencies
php artisan pest:install

Estrutura de Testes

tests/
├── Feature/              # Testes de funcionalidade
│   ├── Http/
│   │   ├── Controllers/
│   │   └── Resources/
│   ├── Actions/
│   ├── Policies/
│   └── Jobs/
├── Unit/                 # Testes unitários
│   ├── Models/
│   ├── Enums/
│   └── Services/
└── Pest.php              # Configuração Pest

Criar Teste

# Feature test
php artisan make:test ProductTest --pest

# Unit test
php artisan make:test Models/ProductTest --pest --unit

Testes de HTTP

Teste Básico de Endpoint

<?php

use function Pest\Laravel\{actingAs, get};
use App\Models\User;
use App\Models\Product;

it('can view products list', function () {
    $user = User::factory()->create();
    Product::factory()->count(3)->create();

    actingAs($user)
        ->get(route('products.index'))
        ->assertOk()
        ->assertSee(__('messages.resources.products'))
        ->assertViewHas('products');
});

it('cannot view products without auth', function () {
    get(route('products.index'))
        ->assertRedirect(route('login'));
});

Teste com Dados

it('can create a product', function () {
    $user = User::factory()->create();
    $category = Category::factory()->create();

    actingAs($user)
        ->post(route('products.store'), [
            'name' => 'Test Product',
            'price' => 100.00,
            'category_id' => $category->id,
        ])
        ->assertRedirect(route('products.index'))
        ->assertSessionHas('success');

    $this->assertDatabaseHas('products', [
        'name' => 'Test Product',
        'price' => 10000, // em centavos
        'category_id' => $category->id,
    ]);
});

Testes de Actions

<?php

use App\Actions\Product\CreateProductAction;
use App\DataObjects\Product\CreateProductData;
use App\Models\Tenant;
use function Pest\Laravel\{assertDatabaseHas};

it('creates a product successfully', function () {
    // Arrange
    $tenant = Tenant::factory()->create();
    $data = CreateProductData::fromRequest([
        'name' => 'Test Product',
        'price' => 100.00,
        'category_id' => Category::factory()->create()->id,
    ]);

    // Act
    $product = CreateProductAction::run($tenant, $data);

    // Assert
    expect($product->name)->toBe('Test Product');
    assertDatabaseHas('products', [
        'id' => $product->id,
        'tenant_id' => $tenant->id,
    ]);
});

it('throws exception when product limit exceeded', function () {
    $tenant = Tenant::factory()->create();
    $tenant->plan->update(['limits' => ['products' => 1]]);

    Product::factory()->create(['tenant_id' => $tenant->id]);

    $data = CreateProductData::fromRequest([
        'name' => 'Second Product',
        'price' => 100.00,
    ]);

    expect(fn() => CreateProductAction::run($tenant, $data))
        ->toThrow(ProductLimitExceededException::class);
});

Testes de Policies

<?php

use App\Models\Product;
use App\Models\User;
use App\Policies\ProductPolicy;

it('allows owner to view product', function () {
    $user = User::factory()->create();
    $product = Product::factory()->create(['tenant_id' => $user->tenant_id]);

    expect((new ProductPolicy)->view($user, $product))->toBeTrue();
});

it('denies non-owner to view product', function () {
    $user = User::factory()->create();
    $otherTenant = Tenant::factory()->create();
    $product = Product::factory()->create(['tenant_id' => $otherTenant->id]);

    expect((new ProductPolicy)->view($user, $product))->toBeFalse();
});

it('allows creation when within limit', function () {
    $user = User::factory()->create();
    $user->tenant->plan->update(['limits' => ['products' => 10]]);

    expect((new ProductPolicy)->create($user))->toBeTrue();
});

Testes de Models

<?php

use App\Models\Product;

it('has a category', function () {
    $product = Product::factory()
        ->for(Category::factory())
        ->create();

    expect($product->category)->toBeInstanceOf(Category::class);
});

it('can check if is active', function () {
    $activeProduct = Product::factory()->active()->create();
    $inactiveProduct = Product::factory()->inactive()->create();

    expect($activeProduct->isActive())->toBeTrue();
    expect($inactiveProduct->isActive())->toBeFalse();
});

it('uses correct table name', function () {
    expect((new Product())->getTable())->toBe('products');
});

Datasets

Criar Dataset

<?php

use App\Enums\ProductStatus;

dataset('product_statuses', [
    'active' => [ProductStatus::ACTIVE, true],
    'inactive' => [ProductStatus::INACTIVE, false],
    'draft' => [ProductStatus::DRAFT, false],
]);

it('correctly determines if product is viewable', function ($status, $expected) {
    $product = Product::factory()->create(['status' => $status]);

    expect($product->isViewable())->toBe($expected);
})->with('product_statuses');

Dataset com Objetos

dataset('users', [
    'admin' => fn() => User::factory()->admin()->create(),
    'regular' => fn() => User::factory()->create(),
    'guest' => null,
]);

it('can access dashboard based on role', function ($user) {
    if ($user) {
        actingAs($user)
            ->get('/dashboard')
            ->assertOk();
    } else {
        get('/dashboard')
            ->assertRedirect('/login');
    }
})->with('users');

Mocking

Mock Events

<?php

use App\Events\ProductCreated;
use Illuminate\Support\Facades\Event;

it('dispatches product created event', function () {
    Event::fake([ProductCreated::class]);

    CreateProductAction::run($tenant, $data);

    Event::assertDispatched(ProductCreated::class, function ($event) use ($product) {
        return $event->product->id === $product->id;
    });
});

it('does not dispatch event on validation error', function () {
    Event::fake([ProductCreated::class]);

    // Tentativa inválida
    // ...

    Event::assertNotDispatched(ProductCreated::class);
});

Mock Mail

<?php

use Illuminate\Support\Facades\Mail;
use App\Mail\ProductCreatedMail;

it('sends email when product is created', function () {
    Mail::fake();

    CreateProductAction::run($tenant, $data);

    Mail::assertSent(ProductCreatedMail::class, function ($mail) use ($product) {
        return $mail->product->id === $product->id;
    });
});

Mock Queue

<?php

use Illuminate\Support\Facades\Queue;
use App\Jobs\ProcessProductImage;

it('queues image processing job', function () {
    Queue::fake();

    CreateProductAction::run($tenant, $data);

    Queue::assertPushed(ProcessProductImage::class, function ($job) use ($product) {
        return $job->product->id === $product->id;
    });
});

Mock Storage

<?php

use Illuminate\Support\Facades\Storage;

it('uploads product image', function () {
    Storage::fake('s3');

    $file = UploadedFile::fake()->image('product.jpg');

    // Upload
    $path = $file->store('products', 's3');

    Storage::disk('s3')->assertExists($path);
});

Mock Notifications

<?php

use Illuminate\Support\Facades\Notification;
use App\Notifications\ProductCreatedNotification;

it('sends notification when product is created', function () {
    Notification::fake();

    CreateProductAction::run($tenant, $data);

    Notification::assertSentTo(
        $tenant->owner,
        ProductCreatedNotification::class
    );
});

Helpers Pest/Laravel

actingAs

actingAs($user)
    ->get('/dashboard')
    ->assertOk();

get, post, put, delete, patch

get('/products')
    ->assertOk();

post('/products', $data)
    ->assertRedirect();

put('/products/1', $data)
    ->assertRedirect();

delete('/products/1')
    ->assertRedirect();

assertDatabaseHas, assertDatabaseMissing

assertDatabaseHas('products', [
    'name' => 'Test Product',
]);

assertDatabaseMissing('products', [
    'name' => 'Deleted Product',
]);

assertSoftDeleted

assertSoftDeleted('products', [
    'id' => $productId,
]);

Testes de Multi-tenancy

<?php

it('tenant can only see their products', function () {
    $tenant1 = Tenant::factory()->create();
    $tenant2 = Tenant::factory()->create();

    Product::factory()->count(3)->create(['tenant_id' => $tenant1->id]);
    Product::factory()->count(5)->create(['tenant_id' => $tenant2->id]);

    actingAs($tenant1->owner)
        ->get(route('products.index'))
        ->assertViewHas('products', function ($products) use ($tenant1) {
            return $products->count() === 3
                && $products->every->tenant_id === $tenant1->id;
        });
});

it('cannot access another tenant product', function () {
    $tenant1 = Tenant::factory()->create();
    $tenant2 = Tenant::factory()->create();
    $product = Product::factory()->create(['tenant_id' => $tenant2->id]);

    actingAs($tenant1->owner)
        ->get(route('products.show', $product))
        ->assertForbidden();
});

Testes de Jobs

<?php

use App\Jobs\SyncProductInventory;
use function Pest\Laravel\{assertDatabaseHas};

it('syncs product inventory', function () {
    $product = Product::factory()->create();

    SyncProductInventory::dispatchSync($product);

    assertDatabaseHas('products', [
        'id' => $product->id,
        'inventory_synced_at' => now(),
    ]);
});

Plugins Recomendados

Pest Parallel

O paralelismo é nativo no Pest. Basta passar a flag --parallel (o Pest usa o ParaTest internamente):

./vendor/bin/pest --parallel

Só instale o brianium/paratest manualmente se o projeto exigir uma versão específica:

composer require brianium/paratest --dev

Coverage

php artisan test --coverage

Melhores Práticas

✅ FAÇA

  • Use testes descritivos com it() e test()
  • Siga padrão Arrange-Act-Assert
  • Use factories para criar dados de teste
  • Use datasets para testar múltiplos cenários
  • Mock externos (Mail, Queue, Storage, Events)
  • Teste exceções com toThrow()
  • Use helpers Pest/Laravel para brevidade
  • Teste authorization em Policies
  • Teste validação em Form Requests
  • Teste multi-tenancy separadamente
  • Mantenha testes rápidos e isolados
  • Use transações para rollback automático

❌ NÃO FAÇA

  • Não teste código de framework
  • Não use lógica complexa em testes
  • Não deixe testes lentos sem otimização
  • Não esqueça de limpar dados após testes
  • Não teste múltiplas coisas em um teste
  • Não dependa de ordem de testes
  • Não use produção database para testes
  • Não esqueça de mock serviços externos

Checklist de Testes

Antes de finalizar QUALQUER feature:

  • Testes de HTTP endpoints criados
  • Testes de Actions criados
  • Testes de Policies criados
  • Testes de Models criados (se necessário)
  • Mocks configurados (Mail, Queue, Events)
  • Datasets para cenários múltiplos
  • Testes de multi-tenancy (se aplicável)
  • Cobertura de código verificada
  • Todos testes passando

Cobertura de Código

# Gerar relatório de cobertura
php artisan test --coverage --min=80

# Gerar HTML coverage
./vendor/bin/pest --coverage --coverage-html=coverage

Referências Cruzadas

  • Actions: Teste Actions criadas com actions
  • Policies: Teste Policies definidas em architecture
  • Exceptions: Teste exceções de exceptions
  • Filament: para testar Resources, use a skill nativa do Laravel Boost (Filament)
  • i18n: Teste traduções de i18n

Referências

Keep looking

Skills are one crate of 627,801. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.