Claude
Skills
Sign in
Back

php-test-writer

Included with Lifetime
$97 forever

Skill for creating and editing PHP tests following project conventions. Use when creating tests, updating test files, or refactoring tests. Applies proper structure, naming, factory usage, and Laravel/PHPUnit best practices.

Code Review

What this skill does


# PHP Test Writer Skill

You are an expert at writing PHP tests for Laravel applications. Your role is to create well-structured, maintainable tests that follow the project's established conventions.

## Test Method Naming - CRITICAL Pattern

**ALWAYS use the `test_` prefix. DO NOT use the `#[Test]` attribute.**

```php
// ✅ CORRECT - Use ONLY test_ prefix
public function test_order_calculates_total_correctly()
{
    // test implementation
}

// ❌ WRONG - Do not use #[Test] attribute
#[Test]
public function test_order_calculates_total_correctly()
{
    // test implementation
}

// ❌ WRONG - Do not use #[Test] without prefix
#[Test]
public function order_calculates_total_correctly()
{
    // test implementation
}
```

**Why:** The project uses the `test_` prefix pattern consistently. While `#[Test]` is valid in PHPUnit, it's unnecessary when using the prefix and adds visual noise to test files.

## Project Context

**Important System Details:**
- **Multitenancy**: Most models have `customer_id` - use `->recycle($customer)` to avoid N+1 customer creation
- **Database Schema**: Uses squashed schema (`database/schema/testing-schema.sql`)
- **Laravel Sail**: All commands must use `./vendor/bin/sail` prefix
- **TestCase Properties**: Feature tests have protected properties like `$customer`, `$user`, `$customerUser` - **DO NOT override these**

## Critical Guidelines

### 1. Always Read TestCase.php First

**MANDATORY**: Before writing any feature test, read `tests/TestCase.php` to understand:
- Protected properties that cannot be overridden
- Available helper methods (e.g., `getCustomer()`, `getAdminUser()`, `actingAsCustomerUser()`)
- Setup methods that run automatically (e.g., `setupGroups()`, `setupCurrencies()`)

```php
// ❌ BAD - Will cause errors
class MyTest extends TestCase
{
    protected $customer; // ERROR: Property already exists in TestCase
}

// ✅ GOOD - Use TestCase helper methods
class MyTest extends TestCase
{
    public function test_something()
    {
        $customer = $this->getCustomer(); // Use TestCase helper
    }
}
```

### 2. File Structure & Naming

**Mirror the app/ directory structure:**
```
app/Services/DataObject/DataObjectService.php
→ tests/Feature/Services/DataObject/DataObjectService/DataObjectServiceTest.php

app/Enums/Filtering/RelativeDatePointEnum.php
→ tests/Unit/Enums/Filtering/RelativeDatePointEnum/RelativeDatePointEnumResolveTest.php
```

**Prefer split over flat structure:**
- When a class has many methods or complex edge cases, create a directory
- Use subdirectories to organize related tests

```
✅ Good (split structure):
tests/Feature/Services/DataObject/DataObjectService/
├── BaseDataObjectServiceTest.php      # Base class
├── Create/
│   ├── BasicCreateTest.php
│   ├── UserColumnTest.php
│   └── FailedOperationTest.php
└── Update/
    ├── BasicUpdateTest.php
    └── UserColumnTest.php

❌ Avoid (flat structure for complex classes):
tests/Feature/Services/DataObject/
└── DataObjectServiceTest.php  # Too much in one file
```

### 3. Test Method Naming

## Test Method Naming - CRITICAL Pattern

**ALWAYS use the `test_` prefix. DO NOT use the `#[Test]` attribute.**

```php
// ✅ CORRECT - Use ONLY test_ prefix
public function test_order_calculates_total_correctly()
{
    // test implementation
}

// ❌ WRONG - Do not use #[Test] attribute
#[Test]
public function test_order_calculates_total_correctly()
{
    // test implementation
}

// ❌ WRONG - Do not use #[Test] without prefix
#[Test]
public function order_calculates_total_correctly()
{
    // test implementation
}
```

**Why:** The project uses the `test_` prefix pattern consistently. While `#[Test]` is valid in PHPUnit, it's unnecessary when using the prefix and adds visual noise to test files.

**Formula**: `test_{methodUnderTest}__{conditions}__{expectedOutput}`

```php
// ✅ Excellent examples:
public function test_update_dispatches_data_object_received_event()
public function test_process_converts_non_string_values_to_strings()
public function test_last_month_with_year_transition()
public function test_attempt_to_create_dataobject_with_existing_extref__throws_error()
public function test_resolve_by_external_id_only_finds_users_for_correct_customer()

// ❌ Avoid:
public function test_update()  // Too vague
public function testUpdateMethod()  // Not descriptive enough
```

**When a whole file tests a single method:**
- Method name can be omitted from test name
- Example: `RelativeDatePointEnumResolveTest.php` tests only `resolve()`, so methods are named like `test_current_quarter_boundaries()`

**Always add PHPDoc:**
```php
/**
 * Test that updating a DataObject dispatches DataObjectReceived event
 */
public function test_update_dispatches_data_object_received_event()
{
    // Test implementation
}
```

### 4. Test Structure: Arrange-Act-Assert

Use the AAA pattern when it makes sense:

```php
public function test_update_object_fields()
{
    // Arrange
    $objectDefinition = $this->getObjectDefinition(
        data_key: 'test_object_update'
    );

    $dataObject = $this->dataObjectService->create(
        objectDefinition: $objectDefinition,
        objectFields: [
            'field1' => 'value1',
            'field2' => 'value2',
        ]
    );

    // Act
    $updatedDataObject = $this->dataObjectService->update(
        dataObject: $dataObject,
        objectFields: [
            'field1' => 'updated_value1',
        ],
        throwOnValidationErrors: true,
    );

    // Assert
    $this->assertEquals('updated_value1', $updatedDataObject->object_fields['field1']);
    $this->assertEquals('value2', $updatedDataObject->object_fields['field2']);
}
```

### 5. Factory Usage

**ALWAYS use factories - NEVER create models manually:**

```php
// ✅ GOOD - Use factories
$customer = Customer::factory()->create();
$user = User::factory()->create();
$customerUser = CustomerUser::factory()
    ->recycle($customer)
    ->recycle($user)
    ->create();

$objectDefinition = ObjectDefinition::factory()
    ->recycle($customer)
    ->create();

// ❌ BAD - Manual creation
$customer = Customer::create(['name' => 'Test Customer']);
$user = new User(['name' => 'Test', 'email' => '[email protected]']);
$user->save();
```

**Use ->recycle() extensively for multitenancy:**

```php
// ✅ EXCELLENT - Recycle customer across all models
$customer = Customer::factory()->create();

$objectDefinition = ObjectDefinition::factory()
    ->recycle($customer)  // Uses same customer
    ->create();

$dataObject = DataObject::factory()
    ->recycle($customer)           // Same customer
    ->recycle($objectDefinition)    // And its nested relations also use same customer
    ->createOneWithService();

// ❌ BAD - Creates multiple customers
$objectDefinition = ObjectDefinition::factory()->create(); // Creates new customer
$dataObject = DataObject::factory()
    ->recycle($objectDefinition)
    ->createOneWithService(); // objectDefinition and dataObject have different customers!
```

**Factory Tips:**
- Check if factories have custom states before manually setting attributes
- Use `->forCustomerUser()`, `->forUserGroup()`, etc. when available
- DataObject uses `->createOneWithService()` or `->createWithService()` instead of `->create()`

### 6. Named Arguments

**Always use named arguments for clarity:**

```php
// ✅ GOOD
$result = $this->processor->process(
    inputValue: 'test',
    processingContext: [],
    objectDefinition: $objectDefinition,
    columnData: $columnData
);

$dataObject = $this->dataObjectService->create(
    objectDefinition: $objectDefinition,
    objectFields: ['name' => 'Test'],
    extRef: 'ext-123',
    visibleRef: 'VIS-123'
);

// ❌ BAD
$result = $this->processor->process('test', [], $objectDefinition, $columnData);
$dataObject = $this->dataObjectService->create($objectDefinition, ['name' => 'Test'], 'ext-123');
```

### 7. Authentication & Session

**Use TestCase helpers:**

```php
// ✅ GOOD - Use TestCase helpers
$customer = $this->getCustomer();
$adminUser =

Related in Code Review