Die Oberfläche ist lokalisiert; Skill-Beschreibungen und -Dokumente werden per KI automatisch in Ihre Sprache übersetzt. Code, Befehle, Pfade und Fachbegriffe bleiben im Original, maßgeblich ist das Dokument des Autors.
Skills entdecken
Daten und Forschung · SKILL.md

kickoff-pest-testing

kickoff-pest-testing

Comprehensive Pest PHP testing skill for Laravel projects on the Kickoff baseline — auto-detects models, controllers, services, and Livewire components then scaffolds matching Pest test files with proper assertions, factories, and database testing patterns. Supports feature tests with actingAs(), API endpoint tests, Livewire::test() component testing, Spati…

Independent GitHub snapshot · Automated classification · Original documentation governs

Skill-Dokumentation

GitHub

name: kickoff-pest-testing metadata: compatible_agents: [claude-code] tags: [laravel, php, pest, testing, livewire, kickoff] description: > Comprehensive Pest PHP testing skill for Laravel projects on the Kickoff baseline — auto-detects models, controllers, services, and Livewire components then scaffolds matching Pest test files with proper assertions, factories, and database testing patterns. Supports feature tests with actingAs(), API endpoint tests, Livewire::test() component testing, Spatie Permission role-based helpers, architecture testing (extending Kickoff baseline), suite performance (seeder placement, Xdebug/pcov, Test Impact Analysis) and coverage gap analysis. Use this skill whenever the user asks to write tests, generate test files, scaffold test suites, check test coverage, speed up a slow suite, or create architecture rules — including: "write tests for this model", "test this controller", "generate feature tests", "add Livewire tests", "scaffold Pest tests", "check test coverage", "the test suite is slow", "set up TIA", "add arch tests", "test this API endpoint", "tulis test untuk model ni", "buat feature test", "tambah test untuk controller", "scaffold test suite", "semak coverage", "buat arch test", "test suite lambat", or "test Livewire component ni". Assumes Pest is already installed with arch testing configured (Kickoff baseline).

Pest Testing Skill (Kickoff baseline)

Auto-detect Laravel application components and scaffold production-quality Pest test files — feature tests, unit tests, Livewire component tests, API tests, and architecture rules. Designed for projects using the Kickoff baseline where Pest and arch testing are pre-configured.

Naming: this skill is kickoff-pest-testing, not pest-testing, because Laravel Boost's boost:install --skills writes its own pest-testing skill into .claude/skills/ and would silently overwrite this one. Boost's covers Pest 4 syntax generically; this one covers scaffolding, the Kickoff arch-test baseline and suite performance. They are complementary — load both.

Command Reference

CommandDescription
/test generateAuto-detect target (model/controller/service/component) and scaffold matching Pest tests
/test featureGenerate feature tests with HTTP assertions, authentication, and database checks
/test unitGenerate unit tests for isolated logic (services, actions, value objects)
/test coverage-checkAnalyse existing tests and report coverage gaps with suggested test stubs

1. /test generate — Auto-Detect & Scaffold

Step 1: Identify the Target

Scan the file or class the user references and classify it:

SignalClassificationTest Location
extends ModelEloquent Modeltests/Feature/Models/
extends Controller or route handlerControllertests/Feature/Http/Controllers/
Class in app/Services/Servicetests/Unit/Services/
Class in app/Actions/Actiontests/Unit/Actions/
extends Component (Livewire)Livewire Componenttests/Feature/Livewire/
Route with api prefix or api.phpAPI Endpointtests/Feature/Api/
Class in app/Policies/Policytests/Feature/Policies/
Blade view onlyViewtests/Feature/Views/
extends MailableMailtests/Feature/Mail/
extends NotificationNotificationtests/Feature/Notifications/
extends Job or implements ShouldQueueJobtests/Feature/Jobs/

Step 2: Read Related Source Files

Before generating tests, read:

  1. The target class itself
  2. Its factory (if model — check database/factories/)
  3. Related form requests (if controller — check app/Http/Requests/)
  4. Related policies (if controller — check app/Policies/)
  5. Route definitions (if controller — check routes/web.php or routes/api.php)
  6. Related Livewire component (if view references wire: directives)

Step 3: Generate Test File

Use the patterns from references/pest-patterns.md. Every generated test file must:

  • Use declare(strict_types=1); at the top
  • Import all classes explicitly (no inline class strings)
  • Use it() syntax, not test() — BDD style
  • Group related tests with describe() blocks
  • Include beforeEach() for shared setup
  • Use factories with states, not manual attribute arrays
  • Assert both happy path and error/validation cases

Step 4: Generate Missing Factories

If the model lacks a factory, generate one at database/factories/{Model}Factory.php with sensible defaults using Faker.


2. /test feature — Feature Tests

Authentication Patterns

use App\Models\User;

beforeEach(function () {
    $this->user = User::factory()->create();
});

it('requires authentication', function () {
    $this->get(route('dashboard'))
        ->assertRedirect(route('login'));
});

it('allows authenticated users to access the dashboard', function () {
    $this->actingAs($this->user)
        ->get(route('dashboard'))
        ->assertOk();
});

Database Assertions

it('creates a new project', function () {
    $this->actingAs($this->user)
        ->post(route('projects.store'), [
            'name' => 'New Project',
            'description' => 'A test project',
        ])
        ->assertRedirect(route('projects.index'));

    $this->assertDatabaseHas('projects', [
        'name' => 'New Project',
        'user_id' => $this->user->id,
    ]);
});

it('soft deletes a project', function () {
    $project = Project::factory()->for($this->user)->create();

    $this->actingAs($this->user)
        ->delete(route('projects.destroy', $project))
        ->assertRedirect();

    $this->assertSoftDeleted($project);
});

Spatie Permission Role-Based Tests

use Spatie\Permission\Models\Role;

beforeEach(function () {
    $this->admin = User::factory()->create();
    $this->admin->assignRole('admin');

    $this->member = User::factory()->create();
    $this->member->assignRole('member');
});

it('allows admins to access user management', function () {
    $this->actingAs($this->admin)
        ->get(route('admin.users.index'))
        ->assertOk();
});

it('denies members access to user management', function () {
    $this->actingAs($this->member)
        ->get(route('admin.users.index'))
        ->assertForbidden();
});

it('allows users with specific permission', function () {
    $this->member->givePermissionTo('view-reports');

    $this->actingAs($this->member)
        ->get(route('reports.index'))
        ->assertOk();
});

Validation Tests

describe('validation', function () {
    it('requires a name', function () {
        $this->actingAs($this->user)
            ->post(route('projects.store'), [
                'name' => '',
            ])
            ->assertSessionHasErrors('name');
    });

    it('requires name to be unique', function () {
        Project::factory()->create(['name' => 'Existing']);

        $this->actingAs($this->user)
            ->post(route('projects.store'), [
                'name' => 'Existing',
            ])
            ->assertSessionHasErrors('name');
    });

    it('rejects names longer than 255 characters', function () {
        $this->actingAs($this->user)
            ->post(route('projects.store'), [
                'name' => str_repeat('a', 256),
            ])
            ->assertSessionHasErrors('name');
    });
});

API Endpoint Tests

use Laravel\Sanctum\Sanctum;

beforeEach(function () {
    $this->user = User::factory()->create();
    Sanctum::actingAs($this->user);
});

it('lists resources as paginated JSON', function () {
    Project::factory()->count(25)->for($this->user)->create();

    $this->getJson(route('api.projects.index'))
        ->assertOk()
        ->assertJsonStructure([
            'data' => [['id', 'name', 'description', 'created_at']],
            'meta' => ['current_page', 'last_page', 'per_page', 'total'],
        ])
        ->assertJsonCount(15, 'data');
});

it('returns 422 for invalid input', function () {
    $this->postJson(route('api.projects.store'), [])
        ->assertUnprocessable()
        ->assertJsonValidationErrors(['name']);
});

it('returns 404 for non-existent resource', function () {
    $this->getJson(route('api.projects.show', 999))
        ->assertNotFound();
});

it('prevents accessing another user resources', function () {
    $otherProject = Project::factory()->create();

    $this->getJson(route('api.projects.show', $otherProject))
        ->assertForbidden();
});

3. /test unit — Unit Tests

Unit tests isolate logic from the framework. Place them in tests/Unit/.

Service Tests

use App\Services\InvoiceCalculator;
use App\Models\Invoice;
use App\Models\InvoiceItem;

beforeEach(function () {
    $this->calculator = new InvoiceCalculator();
});

it('calculates subtotal from line items', function () {
    $invoice = Invoice::factory()
        ->has(InvoiceItem::factory()->count(3)->state([
            'quantity' => 2,
            'unit_price' => 1000, // cents
        ]))
        ->create();

    expect($this->calculator->subtotal($invoice))
        ->toBe(6000);
});

it('applies percentage discount correctly', function () {
    $invoice = Invoice::factory()
        ->has(InvoiceItem::factory()->state([
            'quantity' => 1,
            'unit_price' => 10000,
        ]))
        ->create(['discount_percent' => 10]);

    expect($this->calculator->total($invoice))
        ->toBe(9000);
});

it('never returns negative totals', function () {
    $invoice = Invoice::factory()
        ->has(InvoiceItem::factory()->state([
            'quantity' => 1,
            'unit_price' => 100,
        ]))
        ->create(['discount_percent' => 200]);

    expect($this->calculator->total($invoice))
        ->toBe(0);
});

Action Tests

use App\Actions\CreateTeamAction;
use App\Models\User;
use App\Models\Team;

it('creates a team and assigns the creator as owner', function () {
    $user = User::factory()->create();

    $team = (new CreateTeamAction())->execute(
        user: $user,
        name: 'Engineering',
    );

    expect($team)
        ->toBeInstanceOf(Team::class)
        ->name->toBe('Engineering')
        ->owner_id->toBe($user->id);

    expect($user->fresh()->current_team_id)->toBe($team->id);
});

Value Object Tests

use App\ValueObjects\Money;

it('creates from cents', function () {
    $money = Money::fromCents(1500);

    expect($money->cents())->toBe(1500);
    expect($money->dollars())->toBe(15.00);
    expect($money->formatted())->toBe('$15.00');
});

it('adds two money objects', function () {
    $a = Money::fromCents(1000);
    $b = Money::fromCents(500);

    expect($a->add($b)->cents())->toBe(1500);
});

it('prevents negative money', function () {
    Money::fromCents(-100);
})->throws(InvalidArgumentException::class);

4. /test coverage-check — Coverage Gap Analysis

Step 1: Scan Application Code

Inventory all files in:

  • app/Models/
  • app/Http/Controllers/
  • app/Services/
  • app/Actions/
  • app/Livewire/ or app/Http/Livewire/
  • app/Policies/
  • app/Jobs/
  • app/Mail/
  • app/Notifications/

Step 2: Scan Existing Tests

Map each test file to its target class. Check for:

CheckPass Condition
Test file existsCorresponding test file in tests/Feature/ or tests/Unit/
Happy path coveredAt least one assertOk() or success assertion
Validation coveredassertSessionHasErrors() or assertJsonValidationErrors() for form inputs
Auth coveredassertRedirect(route('login')) or assertUnauthorized() for protected routes
Policy coveredassertForbidden() for policy-protected actions
Factory existsdatabase/factories/{Model}Factory.php exists for each model

Step 3: Report

Test Coverage Gap Report
========================

Models (8 total):
  ✓ User            — tests/Feature/Models/UserTest.php (12 tests)
  ✓ Project         — tests/Feature/Models/ProjectTest.php (8 tests)
  ✗ Invoice         — NO TEST FILE
  ✗ InvoiceItem     — NO TEST FILE
  ~ Team            — tests/Feature/Models/TeamTest.php (2 tests, missing: relationships, scopes)

Controllers (6 total):
  ✓ ProjectController  — tests/Feature/Http/Controllers/ProjectControllerTest.php (15 tests)
  ✗ InvoiceController  — NO TEST FILE
  ~ TeamController     — missing validation tests, missing policy tests

Livewire (4 total):
  ✓ CreateProject     — tests/Feature/Livewire/CreateProjectTest.php (9 tests)
  ✗ ManageTeamMembers — NO TEST FILE

Factories:
  ✗ Invoice          — database/factories/InvoiceFactory.php MISSING
  ✗ InvoiceItem      — database/factories/InvoiceItemFactory.php MISSING

Coverage: 58% of classes have test files (11/19)
Priority: Invoice, InvoiceItem, ManageTeamMembers (high usage, zero tests)

Step 4: Generate Stubs

For each missing test file, offer to generate a stub with:

  • it('has correct fillable attributes') for models
  • it('requires authentication') for controllers
  • it('renders successfully') for Livewire components
  • Appropriate describe() groupings

5. Livewire Component Testing

Read references/livewire-testing.md for full patterns. Key principles:

  • Always use Livewire::test(ComponentClass::class) — never string names
  • Test component state with ->assertSet() and ->assertSee()
  • Test user interactions with ->call(), ->set(), ->toggle()
  • Test events with ->assertDispatched() and ->assertNotDispatched()
  • Test file uploads with UploadedFile::fake()
  • Test Flux UI components via their rendered output
use Livewire\Livewire;
use App\Livewire\CreateProject;

it('renders the create project form', function () {
    Livewire::test(CreateProject::class)
        ->assertStatus(200)
        ->assertSee('Create Project');
});

it('creates a project when form is submitted', function () {
    $this->actingAs($user = User::factory()->create());

    Livewire::test(CreateProject::class)
        ->set('name', 'My Project')
        ->set('description', 'A great project')
        ->call('save')
        ->assertHasNoErrors()
        ->assertDispatched('project-created');

    $this->assertDatabaseHas('projects', [
        'name' => 'My Project',
        'user_id' => $user->id,
    ]);
});

6. Architecture Testing

Read references/arch-testing.md for full patterns. Arch tests enforce project-wide rules that catch issues before code review.

Kickoff Baseline Note

Projects using the Kickoff baseline already have Pest installed with arch testing configured. The file tests/Arch/ArchTest.php exists with baseline rules. Extend it — do not replace.

Common Arch Rules to Add

arch('strict types in all files')
    ->expect('App')
    ->toUseStrictTypes();

arch('no debugging statements')
    ->expect(['dd', 'dump', 'ray', 'var_dump', 'print_r'])
    ->not->toBeUsed();

arch('controllers have correct suffix')
    ->expect('App\Http\Controllers')
    ->toHaveSuffix('Controller');

arch('models extend base model')
    ->expect('App\Models')
    ->toExtend('Illuminate\Database\Eloquent\Model');

arch('no direct DB facade in controllers')
    ->expect('App\Http\Controllers')
    ->not->toUse('Illuminate\Support\Facades\DB');

7. Anti-Patterns to Avoid

When generating tests, never produce code that:

Anti-PatternWhy It Is WrongCorrect Approach
Testing implementation detailsBreaks on refactor, no real confidenceTest behaviour and outcomes
Fragile CSS/DOM selectors->assertSee('<div class="mt-4">') breaks on style changesAssert text content or component state
Missing factoriesManual attribute arrays duplicate schema knowledgeUse factories with states
Testing framework codeit('belongsTo returns relationship') tests Eloquent, not your codeTest business logic that uses the relationship
Mocking everythingOver-mocked tests pass but production breaksMock only external services (APIs, mail, queues)
No assertionsit('runs without errors', fn() => $this->get('/')) proves nothingAlways assert specific outcomes
Seed-dependent testsTests that require php artisan db:seed break in isolationUse factories inside each test
Hardcoded IDsUser::find(1) assumes database stateFactory-create the record in the test
Testing private methodsAccessing privates via reflection is a smellTest through the public interface
Ignoring validationOnly testing happy path misses real bugsAlways test invalid input

8. Test File Naming Convention

TargetTest File Path
App\Models\Usertests/Feature/Models/UserTest.php
App\Http\Controllers\ProjectControllertests/Feature/Http/Controllers/ProjectControllerTest.php
App\Services\InvoiceCalculatortests/Unit/Services/InvoiceCalculatorTest.php
App\Actions\CreateTeamtests/Unit/Actions/CreateTeamTest.php
App\Livewire\CreateProjecttests/Feature/Livewire/CreateProjectTest.php
App\Policies\ProjectPolicytests/Feature/Policies/ProjectPolicyTest.php
App\Jobs\ProcessInvoicetests/Feature/Jobs/ProcessInvoiceTest.php
App\Mail\InvoiceCreatedtests/Feature/Mail/InvoiceCreatedTest.php
Architecture rulestests/Arch/ArchTest.php (extend existing)

9. What a Green Suite Does Not Prove

Three failure modes ship green and are only caught by driving the real page once. When a feature's behaviour lives in Alpine or in a Flux-rendered element, the test to write is the one that pins the markup contract — so a package upgrade fails CI instead of silently returning the feature to nothing.

AssertionWhat it actually provesWhat it does not
assertSee('...')The server rendered that stringNothing about whether the browser does anything with it — a broken Alpine expression, a selector matching zero elements, or an inert x-cloak all pass
assertSee on a Flux controlFlux emitted markupThat your JS can find it. Flux renders <ui-checkbox>, not <input type="checkbox"> — every input[type=checkbox] selector matches zero and reports a confident, wrong answer
A passing test on a wire:model-derived counterThe server computed itThat the operator sees it — wire:model is deferred, so the value is a round trip behind the UI
// Pin the marker the JS depends on, so a Flux upgrade fails here and not in production.
it('renders checkboxes as the flux custom element the panel counts', function () {
    Livewire::test(AllowedComponentsPanel::class)
        ->assertSee('data-flux-checkbox', escape: false);
});

Related: actingAs() sets the user directly on the guard and bypasses session middleware, so a feature test using it passes even when a route is missing the web middleware group. Session and middleware regressions need a real login flow against a running server.


10. Suite Performance

Measure before optimising, but these three account for most of what makes a Kickoff suite slow.

Seeders belong in $seeder, never in a hook

A seeder whose output is identical for every test must not run per test.

// tests/TestCase.php — runs once per process as part of migrate:fresh
protected $seeder = AccessControlSeeder::class;

AccessControlSeeder costs ~645 queries and ~110ms. Called from beforeEach it was roughly half the wall clock of a 1400-test suite — 96s down to 43s once moved. Because every test still runs inside a transaction that rolls back, the visible state is identical either way: the per-test call bought nothing. Check individual files too — a global hook does not stop nine of them re-seeding on top of it.

Xdebug must be pinned off

A machine with xdebug.mode=coverage in its ini pays roughly 3× on every run (one file: 13.7s → 5.3s with XDEBUG_MODE=off). Pin it in composer.json so it cannot be inherited by accident:

"test": ["@putenv XDEBUG_MODE=off", "@php vendor/bin/pest"]

Use the array + @putenv form, not an inline VAR=x prefix: it keeps @php (so Composer picks the PHP binary), works on Windows, and still forwards extra CLI args. And never php -d — ParaTest spawns its workers without the parent's -d flags, so only an environment variable reaches them.

Test Impact Analysis is not free, and its failure mode is silent

TIA records per-test coverage, so it forces a coverage driver on for the whole suite before it can skip anything.

Cold recordReplay
Xdebug~90 min—
pcov~72s~5s

Under Xdebug, Composer's default 300-second process timeout kills the record part way through, leaving worker-edges-*.json and no graph.json — an unusable graph, so the next run re-records from scratch and hits the same wall, forever. That is the whole of the "ran 30 minutes and never finished" report.

Requirements for a working TIA setup:

  • Composer\Config::disableProcessTimeout at the head of the script
  • PHP_INI_SCAN_DIR pointing at a directory containing a pcov ini (environment variable, not -d)
  • TIA opt-in (composer test-tia), never the default composer test — it has a prerequisite
  • tests/Pest.php may set ->filtered(), but deliberately not ->locally() or ->always(), which mark TIA enabled and put the record cost on every local run

The graph lives in ~/.pest/tia/<project-key>/, not in the repo — pest --baseline prints the path, so the absence of a .pest/ directory says nothing. TIA normalises content, so a comment-only edit correctly yields "No affected tests found"; that is not a bug.

Gotcha: pcov built static rather than shared has no get_module symbol, and PHP rejects it with "Invalid library (maybe not a PHP library)" — which reads exactly like "this extension does not support your PHP version" and is not that. Configure with --enable-pcov=shared and check nm -g modules/pcov.so | grep get_module before installing.

SQLite-in-memory is the most permissive engine there is

A green suite is not evidence a migration is portable. Anything touching indexes, foreign keys, or column modification needs a run against the real engine — see the database-conventions reference in project-laravel.



Common Rationalizations

RationalizationReality
"The happy path passes, that's the important one"The happy path is what you already know works. Every production incident is an unhandled path.
"It's too simple to need a test"Then the test is one line and takes thirty seconds. Cost is not the real objection.
"Coverage is at 85%, we're fine"Coverage measures lines executed, not assertions made. A test with no assertion still counts.
"I'll add tests once the feature settles"The feature settles when it ships, and then nobody is paid to go back.
"Mocking it properly takes too long"If a unit is hard to test, that is the design telling you something. Fix the seam, not the test.
"The test is flaky, I'll add a retry"A retry hides a race that production will hit at higher concurrency. Find the shared state.
"I tested it manually"Manual testing is not repeatable, does not run in CI, and does not protect the next person's change.
"Authorization is covered — the owner can view it"The positive case passes even when the check is missing entirely. The test that matters asserts 403 for the wrong user.
"Arch tests are just ceremony"Arch tests are the only thing that catches a convention breaking in a file nobody reviewed.

Red Flags

  • A test with no assertion, or one that only asserts assertTrue(true)
  • ->skip() or ->markTestIncomplete() added during a bug fix
  • sleep() in a test
  • Assertions on id === 1 or on auto-increment ordering
  • now() asserted directly instead of Carbon::setTestNow()
  • Real Mail, Http, Queue or Storage used where a fake exists
  • Tests that pass alone and fail in the suite (or vice versa)
  • A new endpoint with an "owner can access" test and no "other user cannot" test
  • A bug fix PR with no regression test and no explanation of why one was impossible
  • Factories creating dozens of rows per test when two would do

Verification

  • ./vendor/bin/pest green on a clean checkout
  • ./vendor/bin/pest --order-by=random green — no order dependence
  • Every new endpoint has a negative authorization test asserting 403/401
  • Every bug fix has a test that fails when the fix is reverted
  • No sleep(), no ->skip(), no real network or mail in the suite
  • Arch tests pass and cover the new namespaces
  • Suite runtime did not materially regress — if it did, say by how much and why

Reference Files

FileRead When
references/pest-patterns.mdGenerating any Pest test — assertions, datasets, hooks, mocks
references/livewire-testing.mdTesting Livewire components, Flux UI, events, file uploads
references/arch-testing.mdAdding or extending architecture test rules
Gespeicherte Skills