.ai/skills/woocommerce-backend-dev/unit-tests.md
Use this template when creating new test files. It shows all conventions applied together:
<?php
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Tests\Internal\Admin;
use Automattic\WooCommerce\Internal\Admin\OrderProcessor;
use WC_Unit_Test_Case;
/**
* Tests for the OrderProcessor class.
*/
class OrderProcessorTest extends WC_Unit_Test_Case {
/**
* The System Under Test.
*
* @var OrderProcessor
*/
private $sut;
/**
* Set up test fixtures.
*/
public function setUp(): void {
parent::setUp();
$this->sut = new OrderProcessor();
}
/**
* Tear down test fixtures.
*/
public function tearDown(): void {
parent::tearDown();
}
/**
* @testdox Should return true when order is valid.
*/
public function test_returns_true_for_valid_order(): void {
$order = wc_create_order();
$result = $this->sut->is_valid( $order );
$this->assertTrue( $result, 'Valid orders should return true' );
}
/**
* @testdox Should throw exception when order ID is negative.
*/
public function test_throws_exception_for_negative_order_id(): void {
$this->expectException( \InvalidArgumentException::class );
$this->sut->process( -1 );
}
}
| Element | Requirement |
|---|---|
declare( strict_types = 1 ) | Required at file start |
| Namespace | Match source location: Automattic\WooCommerce\Tests\{path} |
| Base class | Extend WC_Unit_Test_Case |
| SUT variable | Use $sut with docblock "The System Under Test." |
| Test docblock | Use @testdox with sentence ending in . |
| Return type | Use void for test methods |
| Assertion messages | Include helpful context for failures |
| Source | Test | Pattern |
|---|---|---|
includes/ classes | tests/php/includes/{path}/class-wc-{name}-test.php | Add -test suffix |
src/ classes | tests/php/src/{path}/{name}Test.php | Append Test (no hyphen) |
Test class: Same name as source class + _Test or Test suffix, extends WC_Unit_Test_Case
Use $sut with docblock "The System Under Test."
/**
* The System Under Test.
*
* @var OrderProcessor
*/
private $sut;
When adding or modifying a unit test method, the part of the docblock that describes the test must be prepended with @testdox. End the comment with . for compliance with linting rules.
Example:
/**
* @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() {
// ...
}
Avoid over-commenting tests. Test names and assertion messages should explain intent.
Good - Self-explanatory:
/**
* @testdox Should return true when order status is draft.
*/
public function test_returns_true_for_draft_orders() {
$order = $this->create_draft_order();
$result = $this->sut->can_delete( $order );
$this->assertTrue( $result, 'Draft orders should be deletable' );
}
Avoid - Over-commented:
/**
* @testdox Should return true when order status is draft.
*/
public function test_returns_true_for_draft_orders() {
// Create a draft order
$order = $this->create_draft_order();
// Call the method we're testing
$result = $this->sut->can_delete( $order );
// Verify the result is true
$this->assertTrue( $result, 'Draft orders should be deletable' );
}
Avoid - Arrange/Act/Assert comments:
// Don't add these structural comments
// Arrange
$order = $this->create_draft_order();
// Act
$result = $this->sut->can_delete( $order );
// Assert
$this->assertTrue( $result );
Use blank lines for visual separation instead. The test structure should be self-evident.
When comments ARE useful in tests:
// Simulate race condition by...// Workaround for WordPress core bug #12345// Payment processor requires 24h holdTest configuration file: phpunit.xml
The PaymentsExtensionSuggestionsTest class demonstrates good testing practices for country-specific functionality.
class PaymentsExtensionSuggestionsTest extends WC_Unit_Test_Case {
/**
* The System Under Test.
*
* @var PaymentsExtensionSuggestions
*/
private $sut;
public function setUp(): void {
parent::setUp();
$this->sut = new PaymentsExtensionSuggestions();
}
/**
* @testdox Should return correct extension count for online merchants by country
* @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}"
);
}
/**
* Data provider for online merchant tests.
*
* @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 ),
// ... more countries
);
}
/**
* @testdox Should return correct extension count for offline merchants by country
* @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}"
);
}
/**
* Data provider for offline merchant tests.
*
* @return array
*/
public function offline_merchant_country_data() {
return array(
'United States' => array( 'US', 2 ),
'United Kingdom' => array( 'GB', 1 ),
'Canada' => array( 'CA', 1 ),
// ... more countries
);
}
}
When working with payment extension suggestions:
src/Internal/Admin/Suggestions/PaymentsExtensionSuggestions.phpWhen testing code that uses wc_get_logger() (directly or via SafeGlobalFunctionProxy::wc_get_logger()), use the woocommerce_logging_class filter to inject a fake logger.
register_legacy_proxy_function_mocks doesn't intercept SafeGlobalFunctionProxy callswc_get_logger()'s internal cacheThe fake logger must implement WC_Logger_Interface. Create an anonymous class with public arrays to track calls ($debug_calls, $warning_calls, etc.) and implement all interface methods (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();
// Inject via filter - passing object bypasses cache.
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' ); // Always clean up.
}
| Aspect | Detail |
|---|---|
| Filter name | woocommerce_logging_class |
| Return value | Object instance (not class name string) |
| Interface | Must implement WC_Logger_Interface |
| Cleanup | Always call remove_all_filters() after test |
See PaymentGatewayTest.php:create_fake_logger() for a complete implementation.