php-test-writer
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePHP 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 prefix. DO NOT use the attribute.
test_#[Test]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 prefix pattern consistently. While is valid in PHPUnit, it's unnecessary when using the prefix and adds visual noise to test files.
test_#[Test]必须使用前缀,禁止使用注解。
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()
{
// 测试实现
}原因: 项目统一使用前缀模式。虽然在PHPUnit中是合法的,但使用前缀时它完全多余,还会给测试文件增加视觉干扰。
test_#[Test]Project Context
项目上下文
Important System Details:
- Multitenancy: Most models have - use
customer_idto avoid N+1 customer creation->recycle($customer) - Database Schema: Uses squashed schema ()
database/schema/testing-schema.sql - Laravel Sail: All commands must use prefix
./vendor/bin/sail - TestCase Properties: Feature tests have protected properties like ,
$customer,$user- DO NOT override these$customerUser
重要系统细节:
- 多租户架构:大多数模型包含- 使用
customer_id避免N+1次客户创建->recycle($customer) - 数据库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 to understand:
tests/TestCase.php- 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.phpPrefer 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 prefix. DO NOT use the attribute.
test_#[Test]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 prefix pattern consistently. While is valid in PHPUnit, it's unnecessary when using the prefix and adds visual noise to test files.
test_#[Test]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 enoughWhen a whole file tests a single method:
- Method name can be omitted from test name
- Example: tests only
RelativeDatePointEnumResolveTest.php, so methods are named likeresolve()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()
{
// 测试实现
}原因: 项目统一使用前缀模式。虽然在PHPUnit中是合法的,但使用前缀时它完全多余,还会给测试文件增加视觉干扰。
test_#[Test]命名公式:
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(), etc. when available->forUserGroup() - DataObject uses or
->createOneWithService()instead of->createWithService()->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 NOT
app()->make()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 THISObjectDefinition Creation:
- ALWAYS use TestCase helper methods for creating ObjectDefinitions
- Helper methods: and
getObjectDefinition()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)); // 应使用assertNotNullAnti-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
undefinedAll 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=testingExamples 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:
- Read TestCase.php to understand available helpers and protected properties
- Check for existing similar tests to follow established patterns
- Read the PHPUnit guidelines at
docs/development/guidelines/php/phpunit-guidelines.md - Determine test type: Feature (integration) or Unit (isolated)
- Create proper directory structure mirroring app/ directory
- Use factories exclusively with for multitenancy
->recycle() - Write descriptive test names following the convention
- Add PHPDoc explaining what the test does
- Use named arguments throughout
- Run the tests to verify they pass
- Consider creating base test class if you have multiple related test files
编写测试时:
- 阅读TestCase.php 以了解可用的辅助方法和受保护属性
- 检查是否存在类似测试 以遵循既定模式
- 阅读PHPUnit指南 位于
docs/development/guidelines/php/phpunit-guidelines.md - 确定测试类型:功能(集成)测试或单元(隔离)测试
- 创建正确的目录结构 镜像app/目录
- 仅使用工厂类 并结合处理多租户场景
->recycle() - 编写描述性的测试名称 遵循命名规范
- 添加PHPDoc注释 说明测试的作用
- 全程使用命名参数
- 运行测试 验证是否通过
- 考虑创建基础测试类 若存在多个相关测试文件
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 - or
getObjectDefinition()getManagedObjectDefinition() - Resolve services with - NOT
app()->make()for type safetyapp() - 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/目录结构 组织测试文件
- 复杂类优先使用拆分结构 而非扁平结构
- 编写完成后运行测试 确保通过
你的目标是创建易于维护、可读性强的测试,以便后续开发者能够轻松理解和扩展。