laravel-testing

Auto-invoked skill

Comprehensive testing with Pest PHP - unit, feature, API, browser

Trigger Keywords

This skill automatically activates when Claude detects these keywords:

test pest coverage tdd assert mock

Overview

The laravel-testing skill provides expertise for writing comprehensive tests using Pest PHP. It covers unit tests, feature tests, API tests, and browser tests with Laravel Dusk.

What This Skill Provides

  • Pest PHP syntax - Modern, expressive test syntax with describe(), it(), expect()
  • Factory patterns - Model factories with states and relationships
  • Database testing - RefreshDatabase, transactions, assertions
  • HTTP testing - Request/response testing with JSON assertions
  • Mocking - Mockery, fakes, and dependency injection
  • Coverage - Code coverage configuration and reporting

Example Conversations

# Writing tests for a feature
"Write tests for my OrderService that handles checkout and payment"

# Testing API endpoints
"Help me test my products API with authentication and validation"

# Mocking external services
"How do I mock the Stripe payment gateway in my tests?"

# Database assertions
"Test that deleting a user cascades to their posts"

Pest PHP Patterns

<?php

uses(RefreshDatabase::class);

describe('OrderService', function () {
    beforeEach(function () {
        $this->user = User::factory()->create();
        $this->service = app(OrderService::class);
    });

    it('creates an order with valid items', function () {
        $items = [
            ['product_id' => 1, 'quantity' => 2],
        ];

        $order = $this->service->create($this->user, $items);

        expect($order)
            ->toBeInstanceOf(Order::class)
            ->user_id->toBe($this->user->id)
            ->items->toHaveCount(1);
    });

    it('throws exception for empty cart', function () {
        $this->service->create($this->user, []);
    })->throws(EmptyCartException::class);

    it('calculates total correctly', function () {
        $order = Order::factory()
            ->has(OrderItem::factory()->count(3)->state(['price' => 10.00]))
            ->create();

        expect($order->total)->toBe(30.00);
    });
});

Test Types

Type Location Use Case
Unit tests/Unit/ Isolated class testing without framework
Feature tests/Feature/ HTTP requests, full application stack
API tests/Feature/Api/ JSON API endpoints with auth
Browser tests/Browser/ Laravel Dusk end-to-end

Testing Events, Jobs & Notifications

use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Notification;

// Testing Events
it('dispatches OrderCreated event', function () {
    Event::fake();

    Order::factory()->create();

    Event::assertDispatched(OrderCreated::class);
});

// Testing Jobs
it('queues SendOrderConfirmation job', function () {
    Queue::fake();

    $order = Order::factory()->create();
    $order->confirm();

    Queue::assertPushed(SendOrderConfirmation::class);
});

// Testing Notifications
it('sends confirmation notification', function () {
    Notification::fake();

    $user = User::factory()->create();
    $order = Order::factory()->for($user)->create();
    $order->sendConfirmation();

    Notification::assertSentTo($user, OrderConfirmation::class);
});

// Testing Exceptions
it('throws exception for invalid state', function () {
    $order = Order::factory()->delivered()->create();

    expect(fn () => $order->cancel())
        ->toThrow(InvalidStateException::class);
});

Common Pitfalls

// 1. Not Using RefreshDatabase - Always reset database state
uses(RefreshDatabase::class);

// 2. Testing Implementation - Test behavior, not implementation
// BAD - testing internal method
expect($service->calculateInternally())->toBe(100);

// GOOD - testing behavior
expect($service->getTotal())->toBe(100);

// 3. Slow Tests - Mock external services
$mock = Mockery::mock(PaymentGateway::class);
$mock->shouldReceive('charge')->once()->andReturn(true);
app()->instance(PaymentGateway::class, $mock);

// 4. Missing Edge Cases - Test boundaries and errors
it('rejects negative quantities', function () {
    $this->postJson('/api/orders', ['quantity' => -1])
        ->assertUnprocessable();
});

// 5. No Assertions - Every test needs clear assertions
// BAD
it('does something', function () {
    $service->process(); // No assertion!
});

// 6. Shared State - Use beforeEach for isolation
beforeEach(function () {
    $this->user = User::factory()->create();
});

Package Integration

  • pestphp/pest - Testing framework
  • pestphp/pest-plugin-laravel - Laravel helpers
  • mockery/mockery - Mocking library
  • laravel/dusk - Browser testing

Best Practices

  • One assertion concept per test
  • Use descriptive test names
  • Test edge cases and errors
  • Keep tests fast (mock slow operations)
  • Use factories for test data
  • Follow Arrange-Act-Assert pattern

Testing HTTP Responses

it('returns products with pagination', function () {
    Product::factory()->count(25)->create();

    $response = $this->getJson('/api/products');

    $response
        ->assertOk()
        ->assertJsonCount(15, 'data')
        ->assertJsonStructure([
            'data' => [['id', 'name', 'price']],
            'meta' => ['total', 'per_page'],
        ]);
});

it('validates required fields', function () {
    $this->actingAs(User::factory()->create())
        ->postJson('/api/products', [])
        ->assertUnprocessable()
        ->assertJsonValidationErrors(['name', 'price']);
});

Related Commands

Related Agent