362 lines
No EOL
10 KiB
Markdown
362 lines
No EOL
10 KiB
Markdown
# 单元测试规范
|
||
|
||
## 目录
|
||
|
||
- [完整测试文件模板](#完整测试文件模板)
|
||
- [测试文件命名和位置](#测试文件命名和位置)
|
||
- [被测系统变量](#被测系统变量)
|
||
- [测试方法文档](#测试方法文档)
|
||
- [测试中的评论](#测试中的评论)
|
||
- [测试配置](#测试配置)
|
||
- [示例:支付扩展建议测试](#示例支付扩展建议测试)
|
||
- [模拟 WooCommerce 日志记录器](#模拟-woocommerce-日志记录器)
|
||
- [通用测试最佳实践](#通用测试最佳实践)
|
||
|
||
## 完整测试文件模板
|
||
|
||
创建新测试文件时请使用此模板。它展示了所有约定规范的应用:
|
||
|
||
```php
|
||
<?php
|
||
declare( strict_types = 1 );
|
||
|
||
namespace Automattic\WooCommerce\Tests\Internal\Admin;
|
||
|
||
use Automattic\WooCommerce\Internal\Admin\OrderProcessor;
|
||
use WC_Unit_Test_Case;
|
||
|
||
/**
|
||
* OrderProcessor 类的测试。
|
||
*/
|
||
class OrderProcessorTest extends WC_Unit_Test_Case {
|
||
|
||
/**
|
||
* 被测系统。
|
||
*
|
||
* @var OrderProcessor
|
||
*/
|
||
private $sut;
|
||
|
||
/**
|
||
* 设置测试固件。
|
||
*/
|
||
public function setUp(): void {
|
||
parent::setUp();
|
||
$this->sut = new OrderProcessor();
|
||
}
|
||
|
||
/**
|
||
* 清理测试固件。
|
||
*/
|
||
public function tearDown(): void {
|
||
parent::tearDown();
|
||
}
|
||
|
||
/**
|
||
* @testdox 当订单有效时应返回 true。
|
||
*/
|
||
public function test_returns_true_for_valid_order(): void {
|
||
$order = wc_create_order();
|
||
|
||
$result = $this->sut->is_valid( $order );
|
||
|
||
$this->assertTrue( $result, '有效订单应返回 true' );
|
||
}
|
||
|
||
/**
|
||
* @testdox 当订单 ID 为负数时应抛出异常。
|
||
*/
|
||
public function test_throws_exception_for_negative_order_id(): void {
|
||
$this->expectException( \InvalidArgumentException::class );
|
||
|
||
$this->sut->process( -1 );
|
||
}
|
||
}
|
||
```
|
||
|
||
### 关键元素
|
||
|
||
| 元素 | 要求 |
|
||
| ------- | ----------- |
|
||
| `declare( strict_types = 1 )` | 在文件开始处必需 |
|
||
| 命名空间 | 匹配源位置:`Automattic\WooCommerce\Tests\{path}` |
|
||
| 基类 | 继承 `WC_Unit_Test_Case` |
|
||
| SUT 变量 | 使用 `$sut` 并附带文档块 "The System Under Test." |
|
||
| 测试文档块 | 使用 `@testdox` 并以 `.` 结尾的句子 |
|
||
| 返回类型 | 为测试方法使用 `void` |
|
||
| 断言消息 | 包含有助于调试失败的上下文 |
|
||
|
||
## 测试文件命名和位置
|
||
|
||
| 来源 | 测试 | 样板 |
|
||
| -------------------- | ---------------------------------------------------- | ------------------------ |
|
||
| `includes/` 类 | `tests/php/includes/{路径}/class-wc-{名称}-test.php` | 添加 `-test` 后缀 |
|
||
| `src/` 类 | `tests/php/src/{路径}/{名称}Test.php` | 追加 `Test` (无连字符)|
|
||
|
||
测试类:与来源类同名 + `_Test` 或 `Test` 后缀,继承 `WC_Unit_Test_Case`
|
||
|
||
## 被测系统变量
|
||
|
||
使用 `$sut` 并添加文档块注释 "The System Under Test."
|
||
|
||
```php
|
||
/**
|
||
* The System Under Test.
|
||
*
|
||
* @var OrderProcessor
|
||
*/
|
||
private $sut;
|
||
```
|
||
|
||
## 测试方法文档
|
||
|
||
当添加或修改单元测试方法时,描述测试的文档块部分必须以 `@testdox` 开头。评论应以 `.` 结尾以符合代码检查规则。
|
||
|
||
**示例:**
|
||
|
||
```php
|
||
/**
|
||
* @testdox Should return true when order is valid.
|
||
*/
|
||
public function test_returns_true_for_valid_order() {
|
||
// ...
|
||
}
|
||
|
||
/**
|
||
* @testdox Should throw exception when order ID is negative.
|
||
*/
|
||
public function test_throws_exception_for_negative_order_id() {
|
||
// ...
|
||
}
|
||
```
|
||
|
||
## 测试中的评论
|
||
|
||
**避免过度注释测试。** 测试名称和断言消息应能解释意图。
|
||
|
||
**良好 - 不言自明:**
|
||
|
||
```php
|
||
/**
|
||
* @testdox 当订单状态为草稿时应返回 true。
|
||
*/
|
||
public function test_returns_true_for_draft_orders() {
|
||
$order = $this->create_draft_order();
|
||
|
||
$result = $this->sut->can_delete( $order );
|
||
|
||
$this->assertTrue( $result, '草稿订单应可删除' );
|
||
}
|
||
```
|
||
|
||
**避免 - 过度注释:**
|
||
|
||
```php
|
||
/**
|
||
* @testdox 当订单状态为草稿时应返回 true。
|
||
*/
|
||
public function test_returns_true_for_draft_orders() {
|
||
// 创建一个草稿订单
|
||
$order = $this->create_draft_order();
|
||
|
||
// 调用我们正在测试的方法
|
||
$result = $this->sut->can_delete( $order );
|
||
|
||
// 验证结果为 true
|
||
$this->assertTrue( $result, '草稿订单应可删除' );
|
||
}
|
||
```
|
||
|
||
**避免 - Arrange/Act/Assert 结构注释:**
|
||
|
||
```php
|
||
// 不要添加这些结构注释
|
||
// Arrange
|
||
$order = $this->create_draft_order();
|
||
|
||
// Act
|
||
$result = $this->sut->can_delete( $order );
|
||
|
||
// Assert
|
||
$this->assertTrue( $result );
|
||
```
|
||
|
||
应使用空行进行视觉分隔。测试结构应是不言自明的。
|
||
|
||
**在测试中评论确实有用的情况:**
|
||
|
||
- 解释复杂的测试设置:`// 通过...模拟竞态条件`
|
||
- 记录已知问题:`// WordPress 核心 bug #12345 的变通方案`
|
||
- 澄清业务规则:`// 支付处理器要求 24 小时保留期`
|
||
|
||
## 测试配置
|
||
|
||
测试配置文件:`phpunit.xml`
|
||
|
||
## 例子:支付扩展建议测试
|
||
|
||
`PaymentsExtensionSuggestionsTest` 类展示了针对国家特定功能进行良好测试的实践。
|
||
|
||
### 已使用的关键字样板
|
||
|
||
1. **数据驱动测试** 使用 PHPUnit 数据提供者
|
||
2. **扩展计数验证** 为不同的商户类型
|
||
3. **清晰的测试组织** 按商户类型(在线/离线)
|
||
|
||
### 测试结构示例
|
||
|
||
```php
|
||
class PaymentsExtensionSuggestionsTest extends WC_Unit_Test_Case {
|
||
/**
|
||
* 被测系统。
|
||
*
|
||
* @var PaymentsExtensionSuggestions
|
||
*/
|
||
private $sut;
|
||
|
||
public function setUp(): void {
|
||
parent::setUp();
|
||
$this->sut = new PaymentsExtensionSuggestions();
|
||
}
|
||
|
||
/**
|
||
* @testdox 应根据国家/地区为在线商家返回正确的扩展数量
|
||
* @dataProvider online_merchant_country_data
|
||
*/
|
||
public function test_get_country_extensions_count_for_online_merchants(
|
||
string $country_code,
|
||
int $expected_count
|
||
) {
|
||
$merchant = array(
|
||
'country' => $country_code,
|
||
'selling_venues' => 'online',
|
||
);
|
||
|
||
$result = $this->sut->get_country_extensions_count( $merchant );
|
||
|
||
$this->assertSame(
|
||
$expected_count,
|
||
$result,
|
||
"Expected {$expected_count} extensions for online merchant in {$country_code}"
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 在线商家测试的数据提供者。
|
||
*
|
||
* @return array
|
||
*/
|
||
public function online_merchant_country_data() {
|
||
return array(
|
||
'United States' => array( 'US', 5 ),
|
||
'United Kingdom' => array( 'GB', 4 ),
|
||
'Canada' => array( 'CA', 3 ),
|
||
'Australia' => array( 'AU', 3 ),
|
||
// ... 更多国家/地区
|
||
);
|
||
}
|
||
|
||
/**
|
||
* @testdox 应根据国家/地区为线下商家返回正确的扩展数量
|
||
* @dataProvider offline_merchant_country_data
|
||
*/
|
||
public function test_get_country_extensions_count_for_offline_merchants(
|
||
string $country_code,
|
||
int $expected_count
|
||
) {
|
||
$merchant = array(
|
||
'country' => $country_code,
|
||
'selling_venues' => 'offline',
|
||
);
|
||
|
||
$result = $this->sut->get_country_extensions_count( $merchant );
|
||
|
||
$this->assertSame(
|
||
$expected_count,
|
||
$result,
|
||
"Expected {$expected_count} extensions for offline merchant in {$country_code}"
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 线下商家测试的数据提供者。
|
||
*
|
||
* @return array
|
||
*/
|
||
public function offline_merchant_country_data() {
|
||
return array(
|
||
'United States' => array( 'US', 2 ),
|
||
'United Kingdom' => array( 'GB', 1 ),
|
||
'Canada' => array( 'CA', 1 ),
|
||
// ... 更多国家/地区
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 支付扩展测试的重要注意事项
|
||
|
||
处理支付扩展建议时:
|
||
|
||
1. **扩展数量必须与实现匹配** 在 `src/Internal/Admin/Suggestions/PaymentsExtensionSuggestions.php` 中
|
||
2. **新增国家时** 在实现中更新测试文件中的两个数据提供器
|
||
3. **测试按商户类型分开**(在线与离线)因为它们有不同的扩展数量
|
||
4. **数据提供器使用描述性关键字**(国家名称)以获得更好的测试输出
|
||
|
||
## 模拟 WooCommerce 日志记录器
|
||
|
||
当测试使用 `wc_get_logger()`(直接或通过 `SafeGlobalFunctionProxy::wc_get_logger()`)的代码时,使用 `woocommerce_logging_class` 过滤器注入一个模拟日志记录器。
|
||
|
||
### 为何采用过滤方法?
|
||
|
||
- `register_legacy_proxy_function_mocks` 无法拦截 `SafeGlobalFunctionProxy` 调用
|
||
- 传递对象(而非类名字符串)会绕过 `wc_get_logger()` 的内部缓存
|
||
|
||
### 创建模拟日志记录器
|
||
|
||
模拟日志记录器必须实现 `WC_Logger_Interface`。创建一个匿名类,其中包含用于跟踪调用的公开数组(`$debug_calls`、`$warning_calls` 等),并实现所有界面方法(`add`、`log`、`debug`、`info`、`warning`、`error`、`emergency`、`alert`、`critical`、`notice`)。
|
||
|
||
### 使用模拟日志记录器
|
||
|
||
```php
|
||
public function test_logs_warning_for_invalid_input(): void {
|
||
$fake_logger = $this->create_fake_logger();
|
||
|
||
// 通过筛选器注入 - 传递对象可绕过缓存。
|
||
add_filter(
|
||
'woocommerce_logging_class',
|
||
function () use ( $fake_logger ) {
|
||
return $fake_logger;
|
||
}
|
||
);
|
||
|
||
$this->sut->process_input( 'invalid-value' );
|
||
|
||
$this->assertCount( 1, $fake_logger->warning_calls );
|
||
|
||
remove_all_filters( 'woocommerce_logging_class' ); // 务必清理。
|
||
}
|
||
```
|
||
|
||
### 关键要点
|
||
|
||
| 方面 | 详情 |
|
||
| ------------ | ----------------------------------------------- |
|
||
| 筛选名称 | `woocommerce_logging_class` |
|
||
| 返回值 | 对象实例(非类名字符串) |
|
||
| 界面 | 必须实现 `WC_Logger_Interface` |
|
||
| 清理 | 测试后总是调用 `remove_all_filters()` |
|
||
|
||
### 参考
|
||
|
||
完整实现请参见 `PaymentGatewayTest.php:create_fake_logger()`。
|
||
|
||
## 通用测试最佳实践
|
||
|
||
1. **进行更改后始终运行测试** 以验证功能
|
||
2. **在开发过程中使用特定的测试过滤器**(参见 woocommerce-dev-cycle 技能中的 running-tests.md)
|
||
3. **编写描述性的测试名称**,以说明正在测试的内容
|
||
4. **使用数据提供器** 来测试具有相同逻辑的多个场景
|
||
5. **包含有用的断言消息**,以便在测试失败时进行调试
|
||
6. **测试成功和失败两种情况**
|
||
7. **模拟外部依赖项**(数据库、API 调用等) |