Initial commit
This commit is contained in:
771
skills/php-test-writer/SKILL.md
Normal file
771
skills/php-test-writer/SKILL.md
Normal file
@@ -0,0 +1,771 @@
|
||||
---
|
||||
name: php-test-writer
|
||||
description: 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.
|
||||
---
|
||||
|
||||
# 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' => 'test@test.com']);
|
||||
$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 = $this->getAdminUser();
|
||||
$adminCustomerUser = $this->getAdminCustomerUser();
|
||||
|
||||
// Acting as a customer user
|
||||
$this->actingAsCustomerUser($adminCustomerUser);
|
||||
|
||||
// Or for session only
|
||||
CustomerSession::store($customer);
|
||||
|
||||
// ❌ BAD - Manual session manipulation
|
||||
session()->put('customer', CustomerSessionData::fromCustomer($customer)->toArray());
|
||||
```
|
||||
|
||||
### 8. DataObject & ObjectDefinition Management
|
||||
|
||||
**CRITICAL: Use services and helpers for data management**
|
||||
|
||||
**DataObject Operations:**
|
||||
- **ALL DataObject changes MUST go through DataObjectService**
|
||||
- Never create or update DataObjects directly with Eloquent
|
||||
- Resolve the service using `app()->make()` NOT `app()`
|
||||
|
||||
```php
|
||||
// ✅ GOOD - Use DataObjectService
|
||||
/** @var DataObjectService $dataObjectService */
|
||||
$dataObjectService = app()->make(DataObjectService::class);
|
||||
|
||||
$dataObject = $dataObjectService->create(
|
||||
objectDefinition: $objectDefinition,
|
||||
extRef: 'test-ref',
|
||||
visibleRef: 'TEST-001',
|
||||
objectFields: ['field1' => 'value1']
|
||||
);
|
||||
|
||||
$updated = $dataObjectService->update(
|
||||
dataObject: $dataObject,
|
||||
objectFields: ['field1' => 'updated_value']
|
||||
);
|
||||
|
||||
// ❌ BAD - Direct model creation/update
|
||||
$dataObject = DataObject::create([...]); // NEVER DO THIS
|
||||
$dataObject->update([...]); // NEVER DO THIS
|
||||
```
|
||||
|
||||
**ObjectDefinition Creation:**
|
||||
- **ALWAYS use TestCase helper methods** for creating ObjectDefinitions
|
||||
- Helper methods: `getObjectDefinition()` and `getManagedObjectDefinition()`
|
||||
- These helpers use ObjectDefinitionService internally
|
||||
|
||||
```php
|
||||
// ✅ GOOD - Use TestCase helper
|
||||
$objectDefinition = $this->getObjectDefinition(
|
||||
data_key: 'test_object',
|
||||
columns: [
|
||||
ObjectDefinitionColumnData::stringColumn(
|
||||
column_key: 'name',
|
||||
column_name: 'Name'
|
||||
),
|
||||
ObjectDefinitionColumnData::decimalColumn(
|
||||
column_key: 'amount',
|
||||
column_name: 'Amount'
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// For managed object definitions (e.g., Integration)
|
||||
$objectDefinition = $this->getManagedObjectDefinition(
|
||||
data_key: 'deal',
|
||||
manageable: $integration,
|
||||
primaryTitleColumn: 'name',
|
||||
columns: [
|
||||
ObjectDefinitionColumnData::stringColumn(
|
||||
column_name: 'name',
|
||||
column_key: 'name'
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// ❌ BAD - Manual creation with factories
|
||||
$objectDefinition = ObjectDefinition::factory()
|
||||
->recycle($this->customer)
|
||||
->create(['data_key' => 'test']);
|
||||
|
||||
ObjectDefinitionColumn::factory()
|
||||
->recycle($objectDefinition)
|
||||
->create(['column_key' => 'test']);
|
||||
```
|
||||
|
||||
**Service Resolution Pattern:**
|
||||
```php
|
||||
// ✅ GOOD - Use app()->make() for type-safe resolution
|
||||
/** @var DataObjectService $dataObjectService */
|
||||
$dataObjectService = app()->make(DataObjectService::class);
|
||||
|
||||
/** @var ObjectDefinitionService $objectDefinitionService */
|
||||
$objectDefinitionService = app()->make(ObjectDefinitionService::class);
|
||||
|
||||
// ❌ BAD - Using app() directly (no type safety)
|
||||
$dataObjectService = app(DataObjectService::class);
|
||||
```
|
||||
|
||||
### 9. Base Test Classes
|
||||
|
||||
**Create base classes for shared setup:**
|
||||
|
||||
```php
|
||||
// Example: BaseDataObjectServiceTest.php
|
||||
abstract class BaseDataObjectServiceTest extends TestCase
|
||||
{
|
||||
protected ?DataObjectService $dataObjectService = null;
|
||||
protected ?ObjectDefinitionService $objectDefinitionService = null;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->setupUserAndCustomer();
|
||||
$this->dataObjectService = app()->make(DataObjectService::class);
|
||||
$this->objectDefinitionService = app()->make(ObjectDefinitionService::class);
|
||||
}
|
||||
}
|
||||
|
||||
// Then extend in specific tests
|
||||
class BasicCreateTest extends BaseDataObjectServiceTest
|
||||
{
|
||||
public function test_something()
|
||||
{
|
||||
// $this->dataObjectService is already available
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Create custom assertion helpers:**
|
||||
|
||||
```php
|
||||
// Example: BaseProcessorTestCase.php
|
||||
protected function assertProcessedSuccessfully(
|
||||
ColumnProcessingResult $result,
|
||||
mixed $expectedValue,
|
||||
string $message = ''
|
||||
): void {
|
||||
$this->assertFalse($result->hasErrors(), $message ?: 'Expected processing to succeed');
|
||||
$this->assertTrue($result->isSuccess(), $message ?: 'Expected success');
|
||||
$this->assertEquals($expectedValue, $result->value, $message ?: 'Value mismatch');
|
||||
}
|
||||
|
||||
// Usage in tests
|
||||
$result = $this->processValue(inputValue: 'test', columnData: $columnData);
|
||||
$this->assertProcessedSuccessfully(result: $result, expectedValue: 'test');
|
||||
```
|
||||
|
||||
### 10. Common Patterns
|
||||
|
||||
**Testing events:**
|
||||
```php
|
||||
Event::fake();
|
||||
|
||||
// ... perform action ...
|
||||
|
||||
Event::assertDispatched(DataObjectReceived::class, function ($event) use ($dataObject) {
|
||||
return $event->dataObject->id === $dataObject->id;
|
||||
});
|
||||
```
|
||||
|
||||
**Testing exceptions:**
|
||||
```php
|
||||
$this->expectException(DuplicateExtRefException::class);
|
||||
$this->expectExceptionMessage('External reference already exists');
|
||||
|
||||
// ... code that should throw ...
|
||||
```
|
||||
|
||||
**Using data providers:**
|
||||
```php
|
||||
/**
|
||||
* @dataProvider nullAndEmptyValueProvider
|
||||
*/
|
||||
public function test_handles_null_and_empty($value)
|
||||
{
|
||||
// Test implementation
|
||||
}
|
||||
|
||||
public static function nullAndEmptyValueProvider(): array
|
||||
{
|
||||
return [
|
||||
'null' => [null],
|
||||
'empty string' => [''],
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### 11. Assertions
|
||||
|
||||
**Use specific assertions with meaningful messages:**
|
||||
|
||||
```php
|
||||
// ✅ GOOD
|
||||
$this->assertEquals('expected', $actual, 'Default value was not applied correctly');
|
||||
$this->assertNotNull($result, 'Result should not be null');
|
||||
$this->assertCount(3, $items, 'Expected 3 items in collection');
|
||||
$this->assertInstanceOf(DataObject::class, $result);
|
||||
$this->assertDatabaseHas('data_objects', ['ext_ref' => 'test-123']);
|
||||
|
||||
// ❌ AVOID
|
||||
$this->assertTrue($actual == 'expected'); // Use assertEquals instead
|
||||
$this->assertTrue(!is_null($result)); // Use assertNotNull instead
|
||||
```
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
### ❌ Hardcoded IDs
|
||||
```php
|
||||
// BAD
|
||||
$dataObject = DataObject::create([
|
||||
'object_definition_id' => 1,
|
||||
'customer_id' => 1,
|
||||
]);
|
||||
|
||||
// GOOD
|
||||
$dataObject = DataObject::factory()
|
||||
->recycle($objectDefinition)
|
||||
->recycle($customer)
|
||||
->createOneWithService();
|
||||
```
|
||||
|
||||
### ❌ Manual Model Creation
|
||||
```php
|
||||
// BAD
|
||||
$user = User::create([
|
||||
'name' => 'Test',
|
||||
'email' => 'test@example.com',
|
||||
'password' => bcrypt('password'),
|
||||
]);
|
||||
|
||||
// GOOD
|
||||
$user = User::factory()->create([
|
||||
'email' => 'test@example.com' // Only specify what matters for the test
|
||||
]);
|
||||
```
|
||||
|
||||
### ❌ Overriding TestCase Protected Properties
|
||||
```php
|
||||
// BAD - Will cause errors
|
||||
class MyTest extends TestCase
|
||||
{
|
||||
protected $customer; // ERROR: Already defined in TestCase
|
||||
protected $user; // ERROR: Already defined in TestCase
|
||||
}
|
||||
|
||||
// GOOD - Use TestCase helpers
|
||||
class MyTest extends TestCase
|
||||
{
|
||||
public function test_something()
|
||||
{
|
||||
$customer = $this->getCustomer();
|
||||
$user = User::factory()->create();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ Using env() Directly
|
||||
```php
|
||||
// BAD
|
||||
$apiKey = env('API_KEY');
|
||||
|
||||
// GOOD
|
||||
$apiKey = config('services.api.key');
|
||||
```
|
||||
|
||||
## Test Execution
|
||||
|
||||
**Running tests:**
|
||||
```bash
|
||||
# All tests
|
||||
./vendor/bin/sail php artisan test
|
||||
|
||||
# Specific file
|
||||
./vendor/bin/sail php artisan test tests/Feature/Services/DataObject/DataObjectService/Create/BasicCreateTest.php
|
||||
|
||||
# Specific test method
|
||||
./vendor/bin/sail php artisan test --filter=test_update_dispatches_data_object_received_event
|
||||
|
||||
# With filter
|
||||
./vendor/bin/sail php artisan test --filter=DataObjectService
|
||||
```
|
||||
|
||||
**Schema regeneration (when migrations change):**
|
||||
```bash
|
||||
./vendor/bin/sail php artisan schema:regenerate-testing --env=testing
|
||||
```
|
||||
|
||||
## Examples from Codebase
|
||||
|
||||
### Feature Test Example (Integration)
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Services\DataObject\DataObjectService\Create;
|
||||
|
||||
use App\Data\ObjectDefinition\ObjectDefinitionColumnData;
|
||||
use App\Exceptions\DataObject\DuplicateExtRefException;
|
||||
use Tests\Feature\Services\DataObject\DataObjectService\BaseDataObjectServiceTest;
|
||||
|
||||
/**
|
||||
* Test basic creation functionality of DataObjectService
|
||||
*/
|
||||
class BasicCreateTest extends BaseDataObjectServiceTest
|
||||
{
|
||||
public function test_attempt_to_create_dataobject_with_existing_extref__throws_error()
|
||||
{
|
||||
$objectDefinition = $this->getObjectDefinition(
|
||||
columns: [
|
||||
ObjectDefinitionColumnData::stringColumn(column_key: 'test_field'),
|
||||
],
|
||||
);
|
||||
|
||||
$this->dataObjectService->create(
|
||||
objectDefinition: $objectDefinition,
|
||||
objectFields: ['test_field' => 'Test Value'],
|
||||
extRef: 'test-create-ref',
|
||||
);
|
||||
|
||||
$this->expectException(DuplicateExtRefException::class);
|
||||
|
||||
// Should throw an exception because the extRef already exists
|
||||
$this->dataObjectService->create(
|
||||
objectDefinition: $objectDefinition,
|
||||
objectFields: ['test_field' => 'Test Value'],
|
||||
extRef: 'test-create-ref',
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Unit Test Example (Isolated)
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Enums\Filtering\RelativeDatePointEnum;
|
||||
|
||||
use App\Enums\Filtering\RelativeDatePointEnum;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Tests\Unit\BaseUnitTestCase;
|
||||
|
||||
class RelativeDatePointEnumResolveTest extends BaseUnitTestCase
|
||||
{
|
||||
/**
|
||||
* Test that context period boundaries resolve correctly
|
||||
*/
|
||||
public function test_context_period_boundaries_resolve_correctly(): void
|
||||
{
|
||||
$periodStart = Carbon::parse('2025-01-01 00:00:00', 'UTC');
|
||||
$periodEnd = Carbon::parse('2025-01-31 23:59:59', 'UTC');
|
||||
|
||||
$startResult = RelativeDatePointEnum::START_OF_CONTEXT_PERIOD->resolve(
|
||||
contextPeriodStart: $periodStart,
|
||||
contextPeriodEnd: $periodEnd
|
||||
);
|
||||
|
||||
$endResult = RelativeDatePointEnum::END_OF_CONTEXT_PERIOD->resolve(
|
||||
contextPeriodStart: $periodStart,
|
||||
contextPeriodEnd: $periodEnd
|
||||
);
|
||||
|
||||
$this->assertEquals('2025-01-01 00:00:00', $startResult->format('Y-m-d H:i:s'));
|
||||
$this->assertEquals('2025-01-31 23:59:59', $endResult->format('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that context period boundaries throw exception when context is missing
|
||||
*/
|
||||
public function test_context_period_boundaries_throw_exception_when_missing(): void
|
||||
{
|
||||
$this->expectException(Exception::class);
|
||||
$this->expectExceptionMessage('Cannot resolve relative date point');
|
||||
|
||||
RelativeDatePointEnum::START_OF_CONTEXT_PERIOD->resolve();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Base Test Class Example
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Services\DataObject\ObjectFields\ColumnTypeProcessors;
|
||||
|
||||
use App\Data\ObjectDefinition\ObjectDefinitionColumnData;
|
||||
use App\Enums\DataObject\Error\DataObjectErrorCode;
|
||||
use App\Enums\ObjectDefinition\ObjectDefinitionColumn\ColumnTypeEnum;
|
||||
use App\Models\ObjectDefinition;
|
||||
use App\Services\DataObject\ObjectFields\ColumnProcessingResult;
|
||||
use App\Services\DataObject\ObjectFields\ColumnTypeProcessors\AbstractColumnProcessor;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Base test case for column processor tests with helpful assertion methods
|
||||
*/
|
||||
abstract class BaseProcessorTestCase extends TestCase
|
||||
{
|
||||
protected AbstractColumnProcessor $processor;
|
||||
|
||||
/**
|
||||
* Create a simple column data object for testing
|
||||
*/
|
||||
protected function makeColumnData(
|
||||
string $columnKey = 'test_field',
|
||||
ColumnTypeEnum $columnType = ColumnTypeEnum::STRING,
|
||||
bool $isRequired = false,
|
||||
mixed $defaultValue = null,
|
||||
?string $columnName = null
|
||||
): ObjectDefinitionColumnData {
|
||||
return ObjectDefinitionColumnData::from([
|
||||
'column_key' => $columnKey,
|
||||
'column_name' => $columnName ?? ucfirst(str_replace('_', ' ', $columnKey)),
|
||||
'column_type' => $columnType,
|
||||
'is_required' => $isRequired,
|
||||
'default_value' => $defaultValue,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a value using the processor with standard test parameters
|
||||
*/
|
||||
protected function processValue(
|
||||
mixed $inputValue,
|
||||
?ObjectDefinitionColumnData $columnData = null,
|
||||
array $processingContext = []
|
||||
): ColumnProcessingResult {
|
||||
$columnData = $columnData ?? $this->makeColumnData();
|
||||
$objectDefinition = \Mockery::mock(ObjectDefinition::class);
|
||||
|
||||
return $this->processor->process(
|
||||
inputValue: $inputValue,
|
||||
processingContext: $processingContext,
|
||||
objectDefinition: $objectDefinition,
|
||||
columnData: $columnData
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that processing was successful and returned the expected value
|
||||
*/
|
||||
protected function assertProcessedSuccessfully(
|
||||
ColumnProcessingResult $result,
|
||||
mixed $expectedValue,
|
||||
string $message = ''
|
||||
): void {
|
||||
$this->assertFalse($result->hasErrors(), $message ?: 'Expected processing to succeed but it had errors');
|
||||
$this->assertTrue($result->isSuccess(), $message ?: 'Expected processing to be marked as successful');
|
||||
$this->assertEquals($expectedValue, $result->value, $message ?: 'Expected processed value did not match');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
When writing tests:
|
||||
|
||||
1. **Read TestCase.php** to understand available helpers and protected properties
|
||||
2. **Check for existing similar tests** to follow established patterns
|
||||
3. **Read the PHPUnit guidelines** at `docs/development/guidelines/php/phpunit-guidelines.md`
|
||||
4. **Determine test type**: Feature (integration) or Unit (isolated)
|
||||
5. **Create proper directory structure** mirroring app/ directory
|
||||
6. **Use factories exclusively** with `->recycle()` for multitenancy
|
||||
7. **Write descriptive test names** following the convention
|
||||
8. **Add PHPDoc** explaining what the test does
|
||||
9. **Use named arguments** throughout
|
||||
10. **Run the tests** to verify they pass
|
||||
11. **Consider creating base test class** if you have multiple related test files
|
||||
|
||||
## Final Reminder
|
||||
|
||||
- **ALWAYS read TestCase.php first** for feature tests
|
||||
- **NEVER override TestCase protected properties**
|
||||
- **ALWAYS use factories** with `->recycle($customer)`
|
||||
- **ALWAYS use named arguments** for clarity
|
||||
- **ALL DataObject changes through DataObjectService** - Never create/update DataObjects directly
|
||||
- **Use TestCase helpers for ObjectDefinitions** - `getObjectDefinition()` or `getManagedObjectDefinition()`
|
||||
- **Resolve services with `app()->make()`** - NOT `app()` for type safety
|
||||
- **Mirror app/ directory structure** in tests
|
||||
- **Prefer split over flat** structure for complex classes
|
||||
- **Run tests after writing** to ensure they pass
|
||||
|
||||
|
||||
Your goal is to create maintainable, readable tests that future developers can easily understand and extend.
|
||||
|
||||
Reference in New Issue
Block a user