10 KiB
单元测试规范
目录
完整测试文件模板
创建新测试文件时请使用此模板。它展示了所有约定规范的应用:
<?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."
/**
* The System Under Test.
*
* @var OrderProcessor
*/
private $sut;
测试方法文档
当添加或修改单元测试方法时,描述测试的文档块部分必须以 @testdox 开头。评论应以 . 结尾以符合代码检查规则。
示例:
/**
* @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() {
// ...
}
测试中的评论
避免过度注释测试。 测试名称和断言消息应能解释意图。
良好 - 不言自明:
/**
* @testdox 当订单状态为草稿时应返回 true。
*/
public function test_returns_true_for_draft_orders() {
$order = $this->create_draft_order();
$result = $this->sut->can_delete( $order );
$this->assertTrue( $result, '草稿订单应可删除' );
}
避免 - 过度注释:
/**
* @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 结构注释:
// 不要添加这些结构注释
// Arrange
$order = $this->create_draft_order();
// Act
$result = $this->sut->can_delete( $order );
// Assert
$this->assertTrue( $result );
应使用空行进行视觉分隔。测试结构应是不言自明的。
在测试中评论确实有用的情况:
- 解释复杂的测试设置:
// 通过...模拟竞态条件 - 记录已知问题:
// WordPress 核心 bug #12345 的变通方案 - 澄清业务规则:
// 支付处理器要求 24 小时保留期
测试配置
测试配置文件:phpunit.xml
例子:支付扩展建议测试
PaymentsExtensionSuggestionsTest 类展示了针对国家特定功能进行良好测试的实践。
已使用的关键字样板
- 数据驱动测试 使用 PHPUnit 数据提供者
- 扩展计数验证 为不同的商户类型
- 清晰的测试组织 按商户类型(在线/离线)
测试结构示例
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 ),
// ... 更多国家/地区
);
}
}
支付扩展测试的重要注意事项
处理支付扩展建议时:
- 扩展数量必须与实现匹配 在
src/Internal/Admin/Suggestions/PaymentsExtensionSuggestions.php中 - 新增国家时 在实现中更新测试文件中的两个数据提供器
- 测试按商户类型分开(在线与离线)因为它们有不同的扩展数量
- 数据提供器使用描述性关键字(国家名称)以获得更好的测试输出
模拟 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)。
使用模拟日志记录器
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()。
通用测试最佳实践
- 进行更改后始终运行测试 以验证功能
- 在开发过程中使用特定的测试过滤器(参见 woocommerce-dev-cycle 技能中的 running-tests.md)
- 编写描述性的测试名称,以说明正在测试的内容
- 使用数据提供器 来测试具有相同逻辑的多个场景
- 包含有用的断言消息,以便在测试失败时进行调试
- 测试成功和失败两种情况
- 模拟外部依赖项(数据库、API 调用等)