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
5.8 KiB
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