Files
bhoza-shift-manager/docs/testing.md
2026-03-09 09:18:21 +00:00

7.9 KiB

Testing with Pest

This template uses Pest for testing - a modern PHP testing framework with elegant syntax.

Quick Start

# Run all tests
make test

# Run with coverage
make test-coverage

# Run specific module tests
make test-module module=Inventory

# Run filtered tests
make test-filter filter="can create"

# Run in parallel (faster)
make test-parallel

Test Structure

tests/
├── Feature/              # HTTP/integration tests
│   └── ExampleTest.php
├── Unit/                 # Isolated unit tests
│   └── ExampleTest.php
├── Modules/              # Module-specific tests
│   └── Inventory/
│       ├── InventoryTest.php
│       └── ProductTest.php
├── Pest.php              # Global configuration
├── TestCase.php          # Base test class
└── TestHelpers.php       # Custom helpers

Writing Tests

Basic Syntax

// Simple test
it('has a welcome page', function () {
    $this->get('/')->assertStatus(200);
});

// With description
test('users can login', function () {
    $user = User::factory()->create();
    
    $this->post('/login', [
        'email' => $user->email,
        'password' => 'password',
    ])->assertRedirect('/dashboard');
});

Expectations

it('can create a product', function () {
    $product = Product::create(['name' => 'Widget', 'price' => 99.99]);
    
    expect($product)
        ->toBeInstanceOf(Product::class)
        ->name->toBe('Widget')
        ->price->toBe(99.99);
});

Grouped Tests

describe('Product Model', function () {
    
    it('can be created', function () {
        // ...
    });
    
    it('has a price', function () {
        // ...
    });
    
    it('belongs to a category', function () {
        // ...
    });
    
});

Setup/Teardown

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

afterEach(function () {
    // Cleanup if needed
});

it('has a user', function () {
    expect($this->user)->toBeInstanceOf(User::class);
});

Global Helpers

Defined in tests/Pest.php:

// Create a regular user
$user = createUser();
$user = createUser(['name' => 'John']);

// Create an admin user
$admin = createAdmin();

Module Testing

When you create a module with php artisan make:module, tests are auto-generated:

tests/Modules/Inventory/
├── InventoryTest.php    # Route and permission tests
└── ProductTest.php      # Model tests (if --model used)

Example Module Test

<?php

use App\Models\User;
use App\Modules\Inventory\Models\Product;

beforeEach(function () {
    $this->user = User::factory()->create();
    $this->user->givePermissionTo([
        'inventory.view',
        'inventory.create',
    ]);
});

describe('Inventory Module', function () {
    
    it('allows authenticated users to view page', function () {
        $this->actingAs($this->user)
            ->get('/inventory')
            ->assertStatus(200);
    });

    it('redirects guests to login', function () {
        $this->get('/inventory')
            ->assertRedirect('/login');
    });

});

describe('Product Model', function () {

    it('can be created', function () {
        $product = Product::create([
            'name' => 'Test Product',
            'price' => 29.99,
        ]);

        expect($product->name)->toBe('Test Product');
    });

    it('is audited on create', function () {
        $this->actingAs($this->user);
        
        $product = Product::create(['name' => 'Audited']);

        $this->assertDatabaseHas('audits', [
            'auditable_type' => Product::class,
            'auditable_id' => $product->id,
            'event' => 'created',
        ]);
    });

});

Testing Helpers

Defined in tests/TestHelpers.php:

use Tests\TestHelpers;

uses(TestHelpers::class)->in('Modules');

// In your test
it('allows users with permission', function () {
    $user = $this->userWithPermissions(['inventory.view']);
    
    $this->actingAs($user)
        ->get('/inventory')
        ->assertStatus(200);
});

it('allows module access', function () {
    $user = $this->userWithModuleAccess('inventory');
    // User has view, create, edit, delete permissions
});

it('audits changes', function () {
    $product = Product::create(['name' => 'Test']);
    
    $this->assertAudited($product, 'created');
});

Testing Livewire Components

use Livewire\Livewire;
use App\Livewire\CreateProduct;

it('can create product via Livewire', function () {
    $this->actingAs(createUser());
    
    Livewire::test(CreateProduct::class)
        ->set('name', 'New Product')
        ->set('price', 49.99)
        ->call('save')
        ->assertHasNoErrors()
        ->assertRedirect('/products');
    
    $this->assertDatabaseHas('products', [
        'name' => 'New Product',
    ]);
});

it('validates required fields', function () {
    Livewire::test(CreateProduct::class)
        ->set('name', '')
        ->call('save')
        ->assertHasErrors(['name' => 'required']);
});

Testing Filament Resources

use App\Modules\Inventory\Models\Product;
use function Pest\Livewire\livewire;

it('can list products in admin', function () {
    $user = createAdmin();
    $products = Product::factory()->count(5)->create();
    
    $this->actingAs($user)
        ->get('/admin/inventory/products')
        ->assertSuccessful()
        ->assertSeeText($products->first()->name);
});

it('can create product from admin', function () {
    $user = createAdmin();
    
    livewire(\App\Modules\Inventory\Filament\Resources\ProductResource\Pages\CreateProduct::class)
        ->fillForm([
            'name' => 'Admin Product',
            'price' => 100,
        ])
        ->call('create')
        ->assertHasNoFormErrors();
    
    $this->assertDatabaseHas('products', [
        'name' => 'Admin Product',
    ]);
});

Database Testing

Tests use SQLite in-memory for speed. The LazilyRefreshDatabase trait only runs migrations when needed.

Factories

// Create model
$product = Product::factory()->create();

// Create multiple
$products = Product::factory()->count(5)->create();

// With attributes
$product = Product::factory()->create([
    'name' => 'Special Product',
    'price' => 999.99,
]);

// With state
$product = Product::factory()->active()->create();

Assertions

// Database has record
$this->assertDatabaseHas('products', [
    'name' => 'Widget',
]);

// Database missing record
$this->assertDatabaseMissing('products', [
    'name' => 'Deleted Product',
]);

// Count records
$this->assertDatabaseCount('products', 5);

HTTP Testing

// GET request
$this->get('/products')->assertStatus(200);

// POST with data
$this->post('/products', [
    'name' => 'New Product',
])->assertRedirect('/products');

// JSON API
$this->getJson('/api/products')
    ->assertStatus(200)
    ->assertJsonCount(5, 'data');

// With auth
$this->actingAs($user)
    ->get('/admin')
    ->assertStatus(200);

// Assert view
$this->get('/products')
    ->assertViewIs('products.index')
    ->assertViewHas('products');

Running Specific Tests

# Single file
php artisan test tests/Feature/ExampleTest.php

# Single test by name
php artisan test --filter="can create product"

# Module tests only
php artisan test tests/Modules/Inventory

# With verbose output
php artisan test --verbose

# Stop on first failure
php artisan test --stop-on-failure

Coverage

# Generate coverage report
make test-coverage

# Requires Xdebug or PCOV
# Coverage report saved to coverage/

Tips

  1. Use factories - Don't manually create models in tests
  2. Test behavior, not implementation - Focus on what, not how
  3. One assertion per test - Keep tests focused
  4. Use descriptive names - it('shows error when email is invalid')
  5. Test edge cases - Empty values, boundaries, errors
  6. Run tests often - Before commits, after changes