php-test-writer

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

PHP Test Writer Skill

PHP测试编写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.
你是Laravel应用的PHP测试专家。你的职责是创建结构清晰、易于维护的测试,且需遵循项目既定规范。

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.
必须使用
test_
前缀,禁止使用
#[Test]
注解。
php
// ✅ 正确写法 - 仅使用test_前缀
public function test_order_calculates_total_correctly()
{
    // 测试实现
}

// ❌ 错误写法 - 禁止使用#[Test]注解
#[Test]
public function test_order_calculates_total_correctly()
{
    // 测试实现
}

// ❌ 错误写法 - 禁止不添加前缀仅使用#[Test]
#[Test]
public function order_calculates_total_correctly()
{
    // 测试实现
}
原因: 项目统一使用
test_
前缀模式。虽然
#[Test]
在PHPUnit中是合法的,但使用前缀时它完全多余,还会给测试文件增加视觉干扰。

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
重要系统细节:
  • 多租户架构:大多数模型包含
    customer_id
    - 使用
    ->recycle($customer)
    避免N+1次客户创建
  • 数据库Schema:使用压缩的schema(
    database/schema/testing-schema.sql
  • Laravel Sail:所有命令必须添加
    ./vendor/bin/sail
    前缀
  • TestCase属性:功能测试包含
    $customer
    $user
    $customerUser
    等受保护属性 - 禁止重写这些属性

Critical Guidelines

核心准则

1. Always Read TestCase.php First

1. 必须先阅读TestCase.php

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
    }
}
强制要求:在编写任何功能测试前,阅读
tests/TestCase.php
以了解:
  • 不可重写的受保护属性
  • 可用的辅助方法(如
    getCustomer()
    getAdminUser()
    actingAsCustomerUser()
  • 自动运行的初始化方法(如
    setupGroups()
    setupCurrencies()
php
// ❌ 错误写法 - 会导致错误
class MyTest extends TestCase
{
    protected $customer; // 错误:该属性已在TestCase中定义
}

// ✅ 正确写法 - 使用TestCase辅助方法
class MyTest extends TestCase
{
    public function test_something()
    {
        $customer = $this->getCustomer(); // 使用TestCase辅助方法
    }
}

2. File Structure & Naming

2. 文件结构与命名

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
镜像app/目录结构:
app/Services/DataObject/DataObjectService.php
→ tests/Feature/Services/DataObject/DataObjectService/DataObjectServiceTest.php

app/Enums/Filtering/RelativeDatePointEnum.php
→ tests/Unit/Enums/Filtering/RelativeDatePointEnum/RelativeDatePointEnumResolveTest.php
优先使用拆分结构而非扁平结构:
  • 当一个类包含多个方法或复杂边缘场景时,创建目录
  • 使用子目录组织相关测试
✅ 良好示例(拆分结构):
tests/Feature/Services/DataObject/DataObjectService/
├── BaseDataObjectServiceTest.php      # 基础类
├── Create/
│   ├── BasicCreateTest.php
│   ├── UserColumnTest.php
│   └── FailedOperationTest.php
└── Update/
    ├── BasicUpdateTest.php
    └── UserColumnTest.php

❌ 需避免(复杂类使用扁平结构):
tests/Feature/Services/DataObject/
└── DataObjectServiceTest.php  # 单个文件内容过多

3. Test Method Naming

3. 测试方法命名

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
}
必须使用
test_
前缀,禁止使用
#[Test]
注解。
php
// ✅ 正确写法 - 仅使用test_前缀
public function test_order_calculates_total_correctly()
{
    // 测试实现
}

// ❌ 错误写法 - 禁止使用#[Test]注解
#[Test]
public function test_order_calculates_total_correctly()
{
    // 测试实现
}

// ❌ 错误写法 - 禁止不添加前缀仅使用#[Test]
#[Test]
public function order_calculates_total_correctly()
{
    // 测试实现
}
原因: 项目统一使用
test_
前缀模式。虽然
#[Test]
在PHPUnit中是合法的,但使用前缀时它完全多余,还会给测试文件增加视觉干扰。
命名公式
test_{被测方法}__{测试条件}__{预期输出}
php
// ✅ 优秀示例:
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()

// ❌ 需避免:
public function test_update()  // 过于模糊
public function testUpdateMethod()  // 描述不够清晰
当整个文件仅测试单个方法时:
  • 测试方法名称中可省略被测方法名
  • 示例:
    RelativeDatePointEnumResolveTest.php
    仅测试
    resolve()
    方法,因此测试方法命名如
    test_current_quarter_boundaries()
必须添加PHPDoc注释:
php
/**
 * 测试更新DataObject时是否会触发DataObjectReceived事件
 */
public function test_update_dispatches_data_object_received_event()
{
    // 测试实现
}

4. Test Structure: Arrange-Act-Assert

4. 测试结构:准备-执行-断言(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']);
}
在合理场景下使用AAA模式:
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

5. 工厂类用法

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()
必须仅使用工厂类 - 禁止手动创建模型:
php
// ✅ 正确写法 - 使用工厂类
$customer = Customer::factory()->create();
$user = User::factory()->create();
$customerUser = CustomerUser::factory()
    ->recycle($customer)
    ->recycle($user)
    ->create();

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

// ❌ 错误写法 - 手动创建
$customer = Customer::create(['name' => 'Test Customer']);
$user = new User(['name' => 'Test', 'email' => 'test@test.com']);
$user->save();
针对多租户架构大量使用->recycle():
php
// ✅ 最佳示例 - 在所有模型中复用同一个customer
$customer = Customer::factory()->create();

$objectDefinition = ObjectDefinition::factory()
    ->recycle($customer)  // 使用同一个customer
    ->create();

$dataObject = DataObject::factory()
    ->recycle($customer)           // 同一个customer
    ->recycle($objectDefinition)    // 其嵌套关联也使用同一个customer
    ->createOneWithService();

// ❌ 错误写法 - 创建多个customer
$objectDefinition = ObjectDefinition::factory()->create(); // 创建新的customer
$dataObject = DataObject::factory()
    ->recycle($objectDefinition)
    ->createOneWithService(); // objectDefinition和dataObject属于不同的customer!
工厂类使用技巧:
  • 在手动设置属性前,检查工厂类是否有自定义状态
  • 当可用时使用
    ->forCustomerUser()
    ->forUserGroup()
    等方法
  • DataObject使用
    ->createOneWithService()
    ->createWithService()
    而非
    ->create()

6. Named Arguments

6. 命名参数

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');
为了清晰性,必须使用命名参数:
php
// ✅ 正确写法
$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'
);

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

7. Authentication & Session

7. 认证与会话

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());
使用TestCase辅助方法:
php
// ✅ 正确写法 - 使用TestCase辅助方法
$customer = $this->getCustomer();
$adminUser = $this->getAdminUser();
$adminCustomerUser = $this->getAdminCustomerUser();

// 模拟以客户用户身份操作
$this->actingAsCustomerUser($adminCustomerUser);

// 或仅存储会话
CustomerSession::store($customer);

// ❌ 错误写法 - 手动操作会话
session()->put('customer', CustomerSessionData::fromCustomer($customer)->toArray());

8. DataObject & ObjectDefinition Management

8. DataObject与ObjectDefinition管理

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);
核心要求:使用服务与辅助方法进行数据管理
DataObject操作:
  • 所有DataObject变更必须通过DataObjectService执行
  • 禁止使用Eloquent直接创建或更新DataObject
  • 使用
    app()->make()
    解析服务,而非直接使用
    app()
php
// ✅ 正确写法 - 使用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']
);

// ❌ 错误写法 - 直接操作模型
$dataObject = DataObject::create([...]);  // 绝对禁止
$dataObject->update([...]);               // 绝对禁止
ObjectDefinition创建:
  • 必须使用TestCase辅助方法创建ObjectDefinition
  • 辅助方法:
    getObjectDefinition()
    getManagedObjectDefinition()
  • 这些辅助方法内部使用ObjectDefinitionService
php
// ✅ 正确写法 - 使用TestCase辅助方法
$objectDefinition = $this->getObjectDefinition(
    data_key: 'test_object',
    columns: [
        ObjectDefinitionColumnData::stringColumn(
            column_key: 'name',
            column_name: 'Name'
        ),
        ObjectDefinitionColumnData::decimalColumn(
            column_key: 'amount',
            column_name: 'Amount'
        ),
    ],
);

// 针对托管ObjectDefinition(如Integration)
$objectDefinition = $this->getManagedObjectDefinition(
    data_key: 'deal',
    manageable: $integration,
    primaryTitleColumn: 'name',
    columns: [
        ObjectDefinitionColumnData::stringColumn(
            column_name: 'name',
            column_key: 'name'
        ),
    ],
);

// ❌ 错误写法 - 使用工厂类手动创建
$objectDefinition = ObjectDefinition::factory()
    ->recycle($this->customer)
    ->create(['data_key' => 'test']);

ObjectDefinitionColumn::factory()
    ->recycle($objectDefinition)
    ->create(['column_key' => 'test']);
服务解析模式:
php
// ✅ 正确写法 - 使用app()->make()实现类型安全解析
/** @var DataObjectService $dataObjectService */
$dataObjectService = app()->make(DataObjectService::class);

/** @var ObjectDefinitionService $objectDefinitionService */
$objectDefinitionService = app()->make(ObjectDefinitionService::class);

// ❌ 错误写法 - 直接使用app()(无类型安全)
$dataObjectService = app(DataObjectService::class);

9. Base Test Classes

9. 基础测试类

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');
为共享初始化逻辑创建基础类:
php
// 示例: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);
    }
}

// 在具体测试中继承
class BasicCreateTest extends BaseDataObjectServiceTest
{
    public function test_something()
    {
        // $this->dataObjectService已可用
    }
}
创建自定义断言辅助方法:
php
// 示例:BaseProcessorTestCase.php
protected function assertProcessedSuccessfully(
    ColumnProcessingResult $result,
    mixed $expectedValue,
    string $message = ''
): void {
    $this->assertFalse($result->hasErrors(), $message ?: '预期处理成功');
    $this->assertTrue($result->isSuccess(), $message ?: '预期处理状态为成功');
    $this->assertEquals($expectedValue, $result->value, $message ?: '值不匹配');
}

// 在测试中使用
$result = $this->processValue(inputValue: 'test', columnData: $columnData);
$this->assertProcessedSuccessfully(result: $result, expectedValue: 'test');

10. Common Patterns

10. 通用模式

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' => [''],
    ];
}
测试事件:
php
Event::fake();

// ... 执行操作 ...

Event::assertDispatched(DataObjectReceived::class, function ($event) use ($dataObject) {
    return $event->dataObject->id === $dataObject->id;
});
测试异常:
php
$this->expectException(DuplicateExtRefException::class);
$this->expectExceptionMessage('External reference already exists');

// ... 执行会抛出异常的代码 ...
使用数据提供者:
php
/**
 * @dataProvider nullAndEmptyValueProvider
 */
public function test_handles_null_and_empty($value)
{
    // 测试实现
}

public static function nullAndEmptyValueProvider(): array
{
    return [
        'null' => [null],
        'empty string' => [''],
    ];
}

11. Assertions

11. 断言

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
使用特定断言并添加有意义的提示信息:
php
// ✅ 正确写法
$this->assertEquals('expected', $actual, '默认值未正确应用');
$this->assertNotNull($result, '结果不应为null');
$this->assertCount(3, $items, '预期集合包含3个元素');
$this->assertInstanceOf(DataObject::class, $result);
$this->assertDatabaseHas('data_objects', ['ext_ref' => 'test-123']);

// ❌ 需避免
$this->assertTrue($actual == 'expected'); // 应使用assertEquals
$this->assertTrue(!is_null($result));     // 应使用assertNotNull

Anti-Patterns to Avoid

需避免的反模式

❌ Hardcoded IDs

❌ 硬编码ID

php
// BAD
$dataObject = DataObject::create([
    'object_definition_id' => 1,
    'customer_id' => 1,
]);

// GOOD
$dataObject = DataObject::factory()
    ->recycle($objectDefinition)
    ->recycle($customer)
    ->createOneWithService();
php
// 错误写法
$dataObject = DataObject::create([
    'object_definition_id' => 1,
    'customer_id' => 1,
]);

// 正确写法
$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
]);
php
// 错误写法
$user = User::create([
    'name' => 'Test',
    'email' => 'test@example.com',
    'password' => bcrypt('password'),
]);

// 正确写法
$user = User::factory()->create([
    'email' => 'test@example.com'  // 仅指定测试所需的属性
]);

❌ Overriding TestCase Protected Properties

❌ 重写TestCase受保护属性

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();
    }
}
php
// 错误写法 - 会导致错误
class MyTest extends TestCase
{
    protected $customer;  // 错误:该属性已在TestCase中定义
    protected $user;      // 错误:该属性已在TestCase中定义
}

// 正确写法 - 使用TestCase辅助方法
class MyTest extends TestCase
{
    public function test_something()
    {
        $customer = $this->getCustomer();
        $user = User::factory()->create();
    }
}

❌ Using env() Directly

❌ 直接使用env()

php
// BAD
$apiKey = env('API_KEY');

// GOOD
$apiKey = config('services.api.key');
php
// 错误写法
$apiKey = env('API_KEY');

// 正确写法
$apiKey = config('services.api.key');

Test Execution

测试执行

Running tests:
bash
undefined
运行测试:
bash
undefined

All tests

所有测试

./vendor/bin/sail php artisan test
./vendor/bin/sail php artisan test

Specific file

指定文件

./vendor/bin/sail php artisan test tests/Feature/Services/DataObject/DataObjectService/Create/BasicCreateTest.php
./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
./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
./vendor/bin/sail php artisan test --filter=DataObjectService

**重新生成Schema(当迁移文件变更时):**
```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',
        );
    }
}
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;

/**
 * 测试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);

        // 由于extRef已存在,此处应抛出异常
        $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();
    }
}
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
{
    /**
     * 测试上下文周期边界解析是否正确
     */
    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'));
    }

    /**
     * 测试当缺少上下文时,上下文周期边界解析是否抛出异常
     */
    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');
    }
}
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;

/**
 * 列处理器测试的基础测试用例,包含实用的断言方法
 */
abstract class BaseProcessorTestCase extends TestCase
{
    protected AbstractColumnProcessor $processor;

    /**
     * 创建用于测试的简单列数据对象
     */
    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,
        ]);
    }

    /**
     * 使用处理器和标准测试参数处理值
     */
    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
        );
    }

    /**
     * 断言处理成功并返回预期值
     */
    protected function assertProcessedSuccessfully(
        ColumnProcessingResult $result,
        mixed $expectedValue,
        string $message = ''
    ): void {
        $this->assertFalse($result->hasErrors(), $message ?: '预期处理成功但出现错误');
        $this->assertTrue($result->isSuccess(), $message ?: '预期处理状态为成功');
        $this->assertEquals($expectedValue, $result->value, $message ?: '预期处理值不匹配');
    }
}

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
编写测试时:
  1. 阅读TestCase.php 以了解可用的辅助方法和受保护属性
  2. 检查是否存在类似测试 以遵循既定模式
  3. 阅读PHPUnit指南 位于
    docs/development/guidelines/php/phpunit-guidelines.md
  4. 确定测试类型:功能(集成)测试或单元(隔离)测试
  5. 创建正确的目录结构 镜像app/目录
  6. 仅使用工厂类 并结合
    ->recycle()
    处理多租户场景
  7. 编写描述性的测试名称 遵循命名规范
  8. 添加PHPDoc注释 说明测试的作用
  9. 全程使用命名参数
  10. 运行测试 验证是否通过
  11. 考虑创建基础测试类 若存在多个相关测试文件

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.
  • 功能测试必须先阅读TestCase.php
  • 绝对禁止重写TestCase受保护属性
  • 必须使用工厂类 并结合
    ->recycle($customer)
  • 必须使用命名参数 以提升可读性
  • 所有DataObject变更必须通过DataObjectService执行 - 禁止直接创建/更新DataObject
  • 使用TestCase辅助方法创建ObjectDefinition -
    getObjectDefinition()
    getManagedObjectDefinition()
  • 使用
    app()->make()
    解析服务
    - 禁止使用
    app()
    以保证类型安全
  • 镜像app/目录结构 组织测试文件
  • 复杂类优先使用拆分结构 而非扁平结构
  • 编写完成后运行测试 确保通过
你的目标是创建易于维护、可读性强的测试,以便后续开发者能够轻松理解和扩展。