All skills
asyrafhussin avatar

/laravel-testing

@a5fe20e

Laravel 13 testing with Pest PHP 4 or PHPUnit 12. Use when writing feature tests, unit tests, or any test code in a Laravel application. Triggers on tasks involving HTTP tests, model factories, database assertions, mocking facades, authentication testing, or test organisation patterns.

Use this Skill: https://skilld.dev/gh/asyrafhussin/agent-skills/laravel-testing

This session only. Nothing lands on disk.

rulespest-hooks.md

≈1.6k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Lifecycle Hooks — beforeEach/afterEach (Pest) or setUp/tearDown (PHPUnit)

Impact: MEDIUM (Eliminates repeated setup code and applies traits cleanly across test files)

Pest: Use beforeEach() for per-test setup and afterEach() for teardown. Use uses() at the top of the file — or globally in Pest.php — to apply traits without repeating them.

PHPUnit: Use setUp() for per-test setup and tearDown() for teardown. Apply traits with use inside the class. Share base setup through a common TestCase subclass.

Bad Example

<?php

// Repeated setup in every test — verbose and fragile (bad in both frameworks)
test('user can view posts', function () {
    $user  = User::factory()->create(); // duplicated
    Mail::fake();                        // duplicated

    $this->actingAs($user)->getJson('/api/posts')->assertOk();
});

test('user can create a post', function () {
    $user = User::factory()->create(); // duplicated
    Mail::fake();                       // duplicated

    $this->actingAs($user)->postJson('/api/posts', ['title' => 'Hi'])->assertStatus(201);
});

Good Example — Pest

<?php

use App\Models\User;
use App\Models\Post;
use Illuminate\Support\Facades\Mail;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

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

test('user can view posts', function () {
    Post::factory()->for($this->user)->count(3)->create();

    $this->actingAs($this->user)
        ->getJson('/api/posts')
        ->assertOk()
        ->assertJsonCount(3, 'data');
});

test('user can create a post', function () {
    $this->actingAs($this->user)
        ->postJson('/api/posts', ['title' => 'Hello', 'body' => 'World'])
        ->assertStatus(201);
});

test('user can delete their post', function () {
    $post = Post::factory()->for($this->user)->create();

    $this->actingAs($this->user)
        ->deleteJson("/api/posts/{$post->id}")
        ->assertNoContent();

    $this->assertModelMissing($post);
});

Apply traits globally in tests/Pest.php:

<?php

// tests/Pest.php — apply RefreshDatabase to all Feature tests globally
uses(
    Tests\TestCase::class,
    Illuminate\Foundation\Testing\RefreshDatabase::class,
)->in('Feature');

uses(Tests\TestCase::class)->in('Unit');

beforeEach inside a describe block:

<?php

uses(RefreshDatabase::class);

describe('admin routes', function () {
    beforeEach(function () {
        $this->admin = User::factory()->admin()->create();
    });

    it('can list all users', function () {
        User::factory()->count(5)->create();

        $this->actingAs($this->admin)
            ->getJson('/api/admin/users')
            ->assertOk();
    });

    it('can delete any user', function () {
        $user = User::factory()->create();

        $this->actingAs($this->admin)
            ->deleteJson("/api/admin/users/{$user->id}")
            ->assertNoContent();
    });
});

Good Example — PHPUnit

<?php

namespace Tests\Feature;

use App\Models\User;
use App\Models\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;

class PostControllerTest extends TestCase
{
    use RefreshDatabase;

    private User $user;

    protected function setUp(): void
    {
        parent::setUp(); // always call parent first

        $this->user = User::factory()->create();
        Mail::fake();
    }

    public function test_user_can_view_posts(): void
    {
        Post::factory()->for($this->user)->count(3)->create();

        $this->actingAs($this->user)
            ->getJson('/api/posts')
            ->assertOk()
            ->assertJsonCount(3, 'data');
    }

    public function test_user_can_create_a_post(): void
    {
        $this->actingAs($this->user)
            ->postJson('/api/posts', ['title' => 'Hello', 'body' => 'World'])
            ->assertStatus(201);
    }

    public function test_user_can_delete_their_post(): void
    {
        $post = Post::factory()->for($this->user)->create();

        $this->actingAs($this->user)
            ->deleteJson("/api/posts/{$post->id}")
            ->assertNoContent();

        $this->assertModelMissing($post);
    }
}

Shared base class for common setup (PHPUnit):

<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

// Base class — apply once, inherit everywhere
abstract class AuthenticatedTestCase extends TestCase
{
    use RefreshDatabase;

    protected \App\Models\User $user;

    protected function setUp(): void
    {
        parent::setUp();
        $this->user = \App\Models\User::factory()->create();
        $this->actingAs($this->user);
    }
}

// Concrete test class — inherits user + auth
class PostControllerTest extends AuthenticatedTestCase
{
    public function test_user_can_create_a_post(): void
    {
        $this->postJson('/api/posts', ['title' => 'Hello', 'body' => 'World'])
            ->assertStatus(201);
    }
}

Key Differences

Pest PHPUnit
Before each test beforeEach(fn() => ...) protected function setUp(): void
After each test afterEach(fn() => ...) protected function tearDown(): void
Global setup uses(...)->in('Feature') in Pest.php Extend a base TestCase class
Trait application uses(RefreshDatabase::class) use RefreshDatabase; in class body
Accessing state in tests $this->property $this->property

Why It Matters

  • DRY: Shared setup belongs in beforeEach/setUp, not copy-pasted in every test
  • Consistency: All tests in the block/class start from the same known state
  • Portability: Logic is identical — only the wrapper syntax differs between frameworks

Reference: Pest PHP — Setup & Teardown · PHPUnit — setUp/tearDown

Source: SKILL.md on GitHub

No alerts17d5 checks · Risk SAFE
  • Gen Agent Trust Hub17d

    This skill is a comprehensive testing guide for Laravel 13, covering both Pest PHP and PHPUnit frameworks. It provides clear rules and code examples for HTTP testing, model factories, database assertions, and mocking services. No security risks or malicious patterns were identified.

  • Socket17d

    No alerts

  • Snyk17d

    Risk: LOW · No issues

  • Runlayer6mo

    25 files scanned · No issues

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at a5fe20e. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub last month.

Steadyupdated 7 months ago
Other metadata
metadata
{
  "author": "Laravel Community",
  "version": "1.1.0",
  "laravelVersion": "13.x",
  "phpVersion": "8.3+",
  "pestVersion": "4.x",
  "phpunitVersion": "12.x"
}

README badge

README badge for asyrafhussin/agent-skills/laravel-testing