Files
APAW/.kilo/skills/php-testing/SKILL.md
¨NW¨ b46a1a20a8 feat: add PHP development stack, atomic tasks, modular code rules, agent monitoring, fix target project detection
7 evolutionary tasks implemented:

1. PHP web development: php-developer agent + 6 skills (Laravel, Symfony, WordPress, security, testing, modular architecture) + 2 pipeline commands (/laravel, /wordpress)

2. Atomic task decomposition: 1 action = 1 task rule, task sizing guide, decomposition protocol for orchestrator, token budgets per complexity

3. Modular code rules: max 100 lines/file, max 30 lines/function, service/repository patterns, cross-module communication via events only

4. Gitea-centric workflow: mandatory issue creation before work, research with links, progress checkboxes, screenshots on test, git history as knowledge base

5. Fix: target project auto-detection — removed all hardcoded UniqueSoft/APAW from API calls, added get_target_repo() via git remote, GITEA_TARGET_REPO env override

6. Agent execution monitoring: agent-executions.jsonl logging, agent-stats.ts statistics script, required fields per invocation, Gitea comment includes duration/tokens

7. Token optimization: 1 action = 1 task principle, token budgets by task type, routing matrix, no scope creep, skip unnecessary pipeline steps
2026-04-18 23:43:04 +01:00

5.8 KiB

name, description
name description
php-testing PHP testing patterns - PHPUnit, Pest, Dusk browser tests, mocking, test organization

PHP Testing Patterns

Project Structure

tests/
├── Unit/
│   ├── Services/
│   │   ├── ProductServiceTest.php
│   │   └── PaymentServiceTest.php
│   └── Repositories/
│       └── ProductRepositoryTest.php
├── Feature/
│   ├── Http/
│   │   ├── ProductControllerTest.php
│   │   └── AuthControllerTest.php
│   └── Console/
│       └── ProcessOrdersCommandTest.php
├── Browser/                   # Dusk tests
│   └── ProductBrowserTest.php
└── TestCase.php               # Base test class

Base TestCase (Laravel)

// tests/TestCase.php
abstract class TestCase extends BaseTestCase
{
    use RefreshDatabase;
    use WithFaker;

    protected function setUp(): void
    {
        parent::setUp();
        // Seed essential data
        $this->seed(DatabaseSeeder::class);
    }
}

PHPUnit Test Patterns

// tests/Unit/Services/ProductServiceTest.php
class ProductServiceTest extends TestCase
{
    private ProductService $service;
    private ProductRepository $repository;

    protected function setUp(): void
    {
        parent::setUp();
        $this->repository = $this->createMock(ProductRepository::class);
        $this->service = new ProductService($this->repository);
    }

    public function testCreateProductWithValidData(): void
    {
        $data = [
            'name' => 'Test Product',
            'price' => 29.99,
            'category_id' => 1,
        ];

        $this->repository
            ->expects($this->once())
            ->method('create')
            ->with($data)
            ->willReturn(new Product($data));

        $result = $this->service->create($data);

        $this->assertInstanceOf(Product::class, $result);
        $this->assertEquals('Test Product', $result->name);
    }

    public function testCreateProductThrowsOnInvalidData(): void
    {
        $this->expectException(ValidationException::class);
        $this->service->create(['name' => '']); // empty name
    }

    /**
     * @dataProvider priceProvider
     */
    public function testPriceCalculation(float $input, float $tax, float $expected): void
    {
        $product = Product::factory()->make(['price' => $input]);
        $result = $this->service->calculateTotal($product, $tax);
        $this->assertEquals($expected, $result);
    }

    public static function priceProvider(): array
    {
        return [
            'no tax' => [100.0, 0.0, 100.0],
            '10% tax' => [100.0, 0.10, 110.0],
            '20% tax' => [100.0, 0.20, 120.0],
        ];
    }
}

Pest Tests (Laravel)

// tests/Feature/ProductTest.php
uses(RefreshDatabase::class);

it('can list products', function () {
    Product::factory()->count(15)->create();

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

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

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

    $response = $this->postJson('/api/v1/products', [
        'name' => 'New Product',
        'price' => 49.99,
        'category_id' => $category->id,
    ]);

    $response->assertCreated()
        ->assertJsonPath('data.name', 'New Product');
});

it('validates required fields', function () {
    $response = $this->postJson('/api/v1/products', []);

    $response->assertUnprocessable()
        ->assertJsonValidationErrors(['name', 'price', 'category_id']);
});

it('requires authentication for protected routes', function () {
    $this->postJson('/api/v1/orders', [])
        ->assertUnauthorized();
});

API Feature Tests

// tests/Feature/AuthTest.php
it('can register', function () {
    $response = $this->postJson('/api/v1/auth/register', [
        'name' => 'Test User',
        'email' => 'test@example.com',
        'password' => 'password123',
        'password_confirmation' => 'password123',
    ]);

    $response->assertCreated()
        ->assertJsonStructure(['user', 'token']);
});

it('can login', function () {
    $user = User::factory()->create([
        'password' => bcrypt('password123'),
    ]);

    $response = $this->postJson('/api/v1/auth/login', [
        'email' => $user->email,
        'password' => 'password123',
    ]);

    $response->assertOk()
        ->assertJsonStructure(['user', 'token']);
});

it('cannot login with wrong password', function () {
    $user = User::factory()->create();

    $this->postJson('/api/v1/auth/login', [
        'email' => $user->email,
        'password' => 'wrong-password',
    ])->assertUnauthorized();
});

Browser Tests (Dusk)

// tests/Browser/ProductBrowserTest.php
class ProductBrowserTest extends DuskTestCase
{
    public function testUserCanBrowseProducts(): void
    {
        $this->browse(function (Browser $browser) {
            $browser->visit('/products')
                ->assertSee('Products')
                ->clickLink('First Product')
                ->assertSee('Add to Cart')
                ->press('Add to Cart')
                ->assertSeeIn('.cart-count', '1');
        });
    }
}

Test Commands

# Run all tests
php artisan test

# Run specific test
php artisan test --filter=ProductServiceTest

# Run with coverage
php artisan test --coverage --min=80

# Pest only
./vendor/bin/pest --parallel

# Dusk browser tests
php artisan dusk

Checklist

  • Unit tests for services (mocked dependencies)
  • Feature tests for API endpoints
  • Browser tests for critical flows
  • Test factories for all models
  • Data providers for edge cases
  • Coverage >= 80%
  • Tests run in CI pipeline
  • Each test is independent and repeatable