Commit e5f81f31f6f for woocommerce
commit e5f81f31f6facb5a8dd21aa32599870171be491e
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date: Thu Aug 13 19:27:32 2026 +0300
[tests] Clear leaked cart and notices in the base test case teardown (#67437)
* Add cart and notice cleanup to the base test case teardown
The cart contents and the queued notices live on the WC() singleton,
which neither the per-test database rollback nor the hook restore
resets, so tests that touch them leak into every later test in the
process. Several test classes carry the same two or three cleanup lines
in their own teardown; centralizing the cleanup covers every test class
and removes that duplication.
Emptying the cart writes to the session via the
woocommerce_cart_emptied callbacks, so both singletons are guarded:
tests such as WC_Tests_Payment_Gateway legitimately null out
WC()->session. The cleanup runs in a try/finally so a cleanup failure
can never skip the parent teardown, whose database rollback and hook
restore every later test depends on.
Randomized-order suite runs confirm the effect: no new failures, and
two previously order-dependent failures (a leaked cart in
WC_Tests_Cart, leaked notices in WC_Tests_Notice_Functions) now pass.
* Remove per-class cleanup covered by the base teardown
With the base test case emptying the cart and clearing notices, and the
parent teardown already rolling back option writes and restoring hooks,
the per-class copies of that cleanup are dead weight: cart emptying and
notice clearing in the Blocks test classes, option backup/restore in
OrderControllerTests and QuantityLimitsTests, and
remove_all_filters/remove_all_actions calls across the touched classes.
Each teardown keeps only what the parent genuinely does not cover:
checkout field deregistration (container-cached CheckoutFields
registry), DI container resets, the cart_context and countries locale
singleton state, and script dequeue/deregister (the $wp_scripts global
is not reset by the WP test framework).
* Add changelog entry for base teardown cleanup
* Reset the cart context and cached locale in the base teardown
The base teardown already cleared the cart contents and the notice
queue, but two more pieces of state live on the WC() singletons and
survive both the database rollback and the hook restore.
WC()->cart->cart_context is set to 'store-api' by the Store API cart
controller and never put back; production code branches on it in
wc-cart-functions.php, WC_Cart and the Blocks ShippingController. In a
sampled run of 2392 tests, 1076 ended with it dirty.
WC()->countries->locale caches the country locale on first read. A test
that reads it while a woocommerce_get_country_locale filter is attached
leaves the filtered value cached: the hook restore removes the filter
but not the value the filter produced. 912 of those 2392 tests ended
with a populated cache. This is reproducible directly, and is why
several test classes carry their own locale reset.
Both properties are declared with defaults, so reset them to those:
'shortcode' and array(). Assignment works even after a test unsets
them, so only the owning singleton needs guarding. Recomputing the
locale costs 0.341ms, paid once by the next test that reads it.
* Remove the per-class resets now covered by the base teardown
Hydration::tearDown() reset the cart context and OrderControllerTests
::tearDown() reset the cached country locale. The base teardown now
does both, so neither method had any unique work left and both are
removed entirely.
The remaining resets elsewhere in the suite are deliberately left in
place. Those in class-wc-cart-test.php, ShippingControllerTest and
OrderConfirmation/Totals run mid-test, to force a recompute while a
filter is attached, which is setup rather than cleanup. Those in the
Store API Checkout and AdditionalFields route tests belong to classes
extending WP_Test_REST_TestCase, which does not inherit this teardown.
* Cover the base test case teardown with a regression test
The teardown clears four pieces of WC() singleton state, and until now
nothing asserted that it does. The suite passing is only indirect
evidence, and a leak reintroduced here surfaces as an order-dependent
failure somewhere unrelated, which is expensive to trace back.
The new pair of tests dirties all four in the first test and asserts
none of it reached the second: cart contents, cart context, the notice
queue, and the cached country locale. The first test asserts its own
fixture, so a failure there reads as the setup breaking rather than the
teardown breaking.
The pair relies on declaration order, which is PHPUnit's default and is
not overridden in this repo. A dependency annotation is deliberately not
used: a failing dependency makes the dependent test skip rather than
fail, which would hide the regression the pair exists to catch.
The locale assertion uses a synthetic label rather than toggling a real
field, so it cannot be satisfied by a WooCommerce default. Both new
assertions were confirmed to fail when the corresponding teardown line
is removed.
* Make the teardown coverage independent of test execution order
The teardown coverage was a pair of tests: the first dirtied the WC()
singletons, the second asserted none of it survived. That only holds
under declaration order. Removing the cart context reset from the
teardown showed the gap: the pair failed under the default order but
passed green under --order-by=reverse, because the assertion test ran
first, against state nothing had dirtied yet. The coverage disappeared
without the suite ever going red.
Nothing in phpunit.xml, the runner scripts or CI sets an execution
order today, so this is latent rather than live. It still matters: an
ad-hoc randomized run is exactly how this kind of leak gets validated,
and a test that quietly stops asserting is worse than no test.
A dependency annotation would pin the order, but it needs
resolveDependencies enabled repo-wide to survive randomization, and it
turns a failed dependency into a skip, so the test still goes quiet
instead of red.
Instead, remove the ordering coupling. The cleanup moves into
clear_wc_singleton_state(), which tearDown() calls, and the test drives
that method directly in one self-contained test. Each of the four
resets was confirmed to fail the test when removed, under both the
default and the reverse order. The previous pair never isolated
empty_cart(); this version does.
* test: Preserve unrelated locale filters in teardown test
The singleton teardown regression test needs to remove its synthetic country-locale callback before checking that the cached value was cleared. It previously removed the entire filter chain, including callbacks that existed before the test began.\n\nKeep a reference to the test-owned closure and remove only that callback. This preserves the production-like hook chain during the final assertion while exercising the same locale-cache cleanup behavior.
* test: Avoid redundant cart teardown work
The base unit-test teardown now owns cleanup of the shared WooCommerce cart, but several subclasses repeated that work and the default persistent cleanup could issue usermeta queries that the parent transaction immediately rolls back.
Use non-persistent cart cleanup in the base helper, cover that contract in the regression test, and remove only the subclass teardown calls it supersedes. This also avoids duplicate cart-empty hooks while preserving setup, mid-test, and independently constructed cart cleanup.
diff --git a/plugins/woocommerce/changelog/fix-tests-base-teardown-singleton-cleanup b/plugins/woocommerce/changelog/fix-tests-base-teardown-singleton-cleanup
new file mode 100644
index 00000000000..bcde5390e89
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-tests-base-teardown-singleton-cleanup
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Clear the WC singleton cart, cart context, notice queue, and cached country locale in the base test case teardown, and remove the per-class cleanup that is now redundant.
diff --git a/plugins/woocommerce/tests/legacy/framework/class-wc-unit-test-case.php b/plugins/woocommerce/tests/legacy/framework/class-wc-unit-test-case.php
index 21d45108e3d..cd7ad7c34a4 100644
--- a/plugins/woocommerce/tests/legacy/framework/class-wc-unit-test-case.php
+++ b/plugins/woocommerce/tests/legacy/framework/class-wc-unit-test-case.php
@@ -147,6 +147,63 @@ class WC_Unit_Test_Case extends WP_HTTP_TestCase {
wc_get_container()->get( LegacyProxy::class )->reset();
}
+ /**
+ * Tear down test case.
+ *
+ * The cart contents, the cart context, the queued notices, and the cached country
+ * locale all live on the WC() singletons, which neither the per-test database
+ * rollback nor the hook restore resets, so clear them here or they leak into every
+ * later test in the process.
+ *
+ * @since 11.1.0
+ */
+ public function tearDown(): void {
+ try {
+ $this->clear_wc_singleton_state();
+ } finally {
+ // The parent teardown must always run: it rolls back the database
+ // transaction and restores the hooks. Skipping it would poison every
+ // test that runs after this one.
+ parent::tearDown();
+ }
+ }
+
+ /**
+ * Clear the WC() singleton state that survives the parent teardown.
+ *
+ * Kept separate from tearDown() so it can be asserted directly, without a test having to
+ * depend on running after another one.
+ *
+ * @since 11.1.0
+ */
+ protected function clear_wc_singleton_state(): void {
+ if ( isset( WC()->cart ) ) {
+ // Emptying the cart writes to the session via the woocommerce_cart_emptied
+ // callbacks, so only do it when that singleton is present too — tests may
+ // legitimately null it out.
+ if ( isset( WC()->session ) ) {
+ // The parent teardown rolls back persistent cart database changes.
+ WC()->cart->empty_cart( false );
+ }
+
+ // Loading a Store API cart route sets this to 'store-api' and never puts it
+ // back, and production code branches on it.
+ WC()->cart->cart_context = 'shortcode';
+ }
+
+ if ( isset( WC()->session ) ) {
+ wc_clear_notices();
+ }
+
+ if ( isset( WC()->countries ) ) {
+ // The locale is cached on first read. A test that reads it while a
+ // woocommerce_get_country_locale filter is attached leaves the filtered
+ // value behind, because the hook restore removes the filter but not the
+ // cache it produced.
+ WC()->countries->locale = array();
+ }
+ }
+
/**
* Fire rest_api_init with only the provided callbacks attached.
*
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/cart/cart.php b/plugins/woocommerce/tests/legacy/unit-tests/cart/cart.php
index 333432179e4..1a396934edb 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/cart/cart.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/cart/cart.php
@@ -18,7 +18,6 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
public function tearDown(): void {
parent::tearDown();
- WC()->cart->empty_cart();
WC()->customer->set_is_vat_exempt( false );
}
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/checkout/checkout.php b/plugins/woocommerce/tests/legacy/unit-tests/checkout/checkout.php
index 658e30cce05..be8e149394c 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/checkout/checkout.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/checkout/checkout.php
@@ -13,14 +13,6 @@ use Automattic\WooCommerce\Enums\OrderStatus;
* Class WC_Checkout
*/
class WC_Tests_Checkout extends WC_Unit_Test_Case {
- /**
- * TearDown.
- */
- public function tearDown(): void {
- parent::tearDown();
- WC()->cart->empty_cart();
- }
-
/**
* Setup.
*/
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/coupon/coupon.php b/plugins/woocommerce/tests/legacy/unit-tests/coupon/coupon.php
index 9e9f6289f83..e6fd2127c68 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/coupon/coupon.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/coupon/coupon.php
@@ -28,7 +28,6 @@ class WC_Tests_Coupon extends WC_Unit_Test_Case {
* Cleans up after the test class.
*/
public function tearDown(): void {
- WC()->cart->empty_cart();
WC()->cart->remove_coupons();
parent::tearDown();
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/discounts/discounts.php b/plugins/woocommerce/tests/legacy/unit-tests/discounts/discounts.php
index 8a4320ac210..b2448d8f8d5 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/discounts/discounts.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/discounts/discounts.php
@@ -77,7 +77,6 @@ class WC_Tests_Discounts extends WC_Unit_Test_Case {
* Clean up after each test. DB changes are reverted in parent::tearDown().
*/
public function tearDown(): void {
- WC()->cart->empty_cart();
WC()->cart->remove_coupons();
parent::tearDown();
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/totals/totals.php b/plugins/woocommerce/tests/legacy/unit-tests/totals/totals.php
index 59c8295cfc4..9599bab8a96 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/totals/totals.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/totals/totals.php
@@ -116,7 +116,6 @@ class WC_Tests_Totals extends WC_Unit_Test_Case {
* Clean up after test.
*/
public function tearDown(): void {
- WC()->cart->empty_cart();
WC()->session->set( 'chosen_shipping_methods', array() );
WC_Helper_Shipping::delete_simple_flat_rate();
remove_action( 'woocommerce_cart_calculate_fees', array( $this, 'add_cart_fees_callback' ) );
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-cart-shipping-rounding-test.php b/plugins/woocommerce/tests/php/includes/class-wc-cart-shipping-rounding-test.php
index b47762379c0..4644f170d1e 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-cart-shipping-rounding-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-cart-shipping-rounding-test.php
@@ -62,8 +62,6 @@ class WC_Cart_Shipping_Rounding_Test extends WC_Unit_Test_Case {
*/
public function tearDown(): void {
try {
- WC()->cart->empty_cart();
-
if ( $this->zone ) {
$this->zone->delete();
}
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php b/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php
index fef08aa2d27..0ae20d8f236 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php
@@ -35,7 +35,6 @@ class WC_Cart_Test extends \WC_Unit_Test_Case {
public function tearDown(): void {
parent::tearDown();
- WC()->cart->empty_cart();
WC()->customer->set_is_vat_exempt( false );
WC()->session->set( 'wc_notices', null );
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-cart-totals-test.php b/plugins/woocommerce/tests/php/includes/class-wc-cart-totals-test.php
index 27638079c3e..86d060b8c7a 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-cart-totals-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-cart-totals-test.php
@@ -27,7 +27,6 @@ class WC_Cart_Totals_Tests extends WC_Unit_Test_Case {
*/
public function tearDown(): void {
parent::tearDown();
- WC()->cart->empty_cart();
WC()->shipping()->enabled = $this->shipping_was_enabled;
}
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-discounts-tests.php b/plugins/woocommerce/tests/php/includes/class-wc-discounts-tests.php
index c00168278c5..1260763ca35 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-discounts-tests.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-discounts-tests.php
@@ -15,11 +15,10 @@ class WC_Discounts_Tests extends WC_Unit_Test_Case {
/**
* Tear down test fixtures.
*
- * The cart and current user are in-memory globals that the per-test DB transaction
- * does not roll back, so reset them explicitly to avoid leaking state into other tests.
+ * The current user is an in-memory global that the per-test DB transaction does not
+ * roll back, so reset it explicitly to avoid leaking state into other tests.
*/
public function tearDown(): void {
- WC()->cart->empty_cart();
wp_set_current_user( 0 );
parent::tearDown();
}
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-tax-test.php b/plugins/woocommerce/tests/php/includes/class-wc-tax-test.php
index efd7f3a2206..ecfb4d3d749 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-tax-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-tax-test.php
@@ -67,9 +67,6 @@ class WC_Tax_Test extends WC_Unit_Test_Case {
public function tearDown(): void {
parent::tearDown();
- // Clear cart.
- WC()->cart->empty_cart();
-
remove_all_filters( 'woocommerce_shipping_tax_class' );
remove_all_filters( 'woocommerce_shipping_prices_include_tax' );
diff --git a/plugins/woocommerce/tests/php/includes/wc-order-functions-test.php b/plugins/woocommerce/tests/php/includes/wc-order-functions-test.php
index 657309492ee..8de6654215a 100644
--- a/plugins/woocommerce/tests/php/includes/wc-order-functions-test.php
+++ b/plugins/woocommerce/tests/php/includes/wc-order-functions-test.php
@@ -13,14 +13,6 @@ use Automattic\WooCommerce\Internal\Utilities\Users;
* Class WC_Order_Functions_Test
*/
class WC_Order_Functions_Test extends \WC_Unit_Test_Case {
- /**
- * tearDown.
- */
- public function tearDown(): void {
- parent::tearDown();
- WC()->cart->empty_cart();
- }
-
/**
* Test that wc_restock_refunded_items() preserves order item stock metadata.
*/
diff --git a/plugins/woocommerce/tests/php/includes/wc-stock-functions-tests.php b/plugins/woocommerce/tests/php/includes/wc-stock-functions-tests.php
index 0adf6efcf40..c05b122c787 100644
--- a/plugins/woocommerce/tests/php/includes/wc-stock-functions-tests.php
+++ b/plugins/woocommerce/tests/php/includes/wc-stock-functions-tests.php
@@ -49,7 +49,6 @@ class WC_Stock_Functions_Tests extends \WC_Unit_Test_Case {
*/
public function tearDown(): void {
parent::tearDown();
- WC()->cart->empty_cart();
$this->stock_product = null;
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutFieldsFrontendTest.php b/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutFieldsFrontendTest.php
index 5dbe5a574ac..916ecf9a621 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutFieldsFrontendTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutFieldsFrontendTest.php
@@ -53,10 +53,6 @@ class CheckoutFieldsFrontendTest extends \WC_Unit_Test_Case {
* Tear down.
*/
public function tearDown(): void {
- remove_filter( 'woocommerce_add_notice', [ $this, 'capture_notice' ] );
- remove_filter( 'woocommerce_add_error', [ $this, 'capture_error' ] );
- remove_filter( 'woocommerce_add_success', [ $this, 'capture_success' ] );
-
foreach ( $this->registered_fields as $field_id ) {
__internal_woocommerce_blocks_deregister_checkout_field( $field_id );
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutFieldsSchema/DocumentObjectTests.php b/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutFieldsSchema/DocumentObjectTests.php
index 11cc3041235..2f49d42df9a 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutFieldsSchema/DocumentObjectTests.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutFieldsSchema/DocumentObjectTests.php
@@ -120,11 +120,8 @@ class DocumentObjectTests extends \WC_Unit_Test_Case {
* Tear down the test environment.
*/
public function tearDown(): void {
- // The cart, the notice queue, and the CheckoutFields registry all live on singletons
- // that neither the database rollback nor the hook restore touches, so reset them
- // unconditionally before handing back to the parent.
- wc_empty_cart();
- wc_clear_notices();
+ // The CheckoutFields registry lives on the container-cached singleton, which the
+ // parent teardown does not reset, so deregister the fields unconditionally.
$this->additional_fields_controller->deregister_checkout_field( 'namespace/contact_field' );
$this->additional_fields_controller->deregister_checkout_field( 'namespace/order_field' );
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutLinkTest.php b/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutLinkTest.php
index 598ba253677..df77bcb6ca7 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutLinkTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutLinkTest.php
@@ -10,19 +10,6 @@ use Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper;
* Unit tests for CheckoutLink.
*/
class CheckoutLinkTest extends \WC_Unit_Test_Case {
- /**
- * Tear down the test environment.
- */
- public function tearDown(): void {
- // The cart and the notice queue live on the WC singleton, so the rollback does not
- // clear them. Without this the products this test adds stay in the cart, and the
- // coupon success notice stays queued, for every later test in the process.
- WC()->cart->empty_cart();
- wc_clear_notices();
-
- parent::tearDown();
- }
-
/**
* Test that products and coupon are added and token in url.
*/
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/Hydration.php b/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/Hydration.php
index 71b54d79ed7..856d734696c 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/Hydration.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/Hydration.php
@@ -25,21 +25,6 @@ class Hydration extends \WC_Unit_Test_Case {
$this->sut = Package::container()->get( \Automattic\WooCommerce\Blocks\Domain\Services\Hydration::class );
}
- /**
- * Restore the global cart.
- *
- * Loading a Store API cart route sets `cart_context` to 'store-api' on the WC cart
- * singleton and never puts it back. Neither the database rollback nor the hook restore
- * covers that, and production code branches on it, so reset it here.
- */
- public function tearDown(): void {
- WC()->cart->empty_cart();
- WC()->cart->cart_context = 'shortcode';
-
- parent::tearDown();
- }
-
-
/**
* @testDox REST API response is returned without loading entire REST server.
*/
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/CartControllerTests.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/CartControllerTests.php
index ef61b14c34f..ba8a04cef5e 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/CartControllerTests.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/CartControllerTests.php
@@ -15,8 +15,6 @@ class CartControllerTests extends \WC_Unit_Test_Case {
* tearDown.
*/
public function tearDown(): void {
- WC()->cart->empty_cart();
-
// Reset DI container to clear any mocks.
$container = wc_get_container();
$container->reset_all_resolved();
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/NoticeHandlerTests.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/NoticeHandlerTests.php
index 16d7cacb79a..ea75f41038b 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/NoticeHandlerTests.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/NoticeHandlerTests.php
@@ -11,19 +11,6 @@ use Automattic\WooCommerce\StoreApi\Utilities\NoticeHandler;
*/
class NoticeHandlerTests extends \WC_Unit_Test_Case {
- /**
- * Clear the WooCommerce notice queue.
- *
- * Notices live on the WC session singleton, which neither the per-test database
- * transaction nor the hook restore touches, so they have to be cleared explicitly or
- * they leak into every later test in the process.
- */
- public function tearDown(): void {
- wc_clear_notices();
-
- parent::tearDown();
- }
-
/**
* Test convert_notices_to_exceptions.
*/
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/OrderControllerTests.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/OrderControllerTests.php
index cae507f6c50..16dd166b822 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/OrderControllerTests.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/OrderControllerTests.php
@@ -14,20 +14,6 @@ use Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper;
* OrderControllerTests class.
*/
class OrderControllerTests extends \WC_Unit_Test_Case {
- /**
- * Whether the checkout phone field option existed before the test.
- *
- * @var bool
- */
- private $checkout_phone_field_option_existed = false;
-
- /**
- * Checkout phone field option value before the test.
- *
- * @var mixed
- */
- private $checkout_phone_field_option_value;
-
/**
* The system under test.
*
@@ -43,13 +29,10 @@ class OrderControllerTests extends \WC_Unit_Test_Case {
public function setUp(): void {
parent::setUp();
- $missing_option = new \stdClass();
- $this->checkout_phone_field_option_value = get_option( 'woocommerce_checkout_phone_field', $missing_option );
- $this->checkout_phone_field_option_existed = $missing_option !== $this->checkout_phone_field_option_value;
-
// The fixtures in this class do not provide phone numbers, so make the
// phone field optional as other Store API test classes do. Without this
// the class only passes when run after a class that already did so.
+ // The per-test database rollback restores the option.
update_option( 'woocommerce_checkout_phone_field', 'optional' );
$this->sut = new class() extends OrderController {
@@ -66,28 +49,6 @@ class OrderControllerTests extends \WC_Unit_Test_Case {
};
}
- /**
- * Tear down after test.
- */
- public function tearDown(): void {
- try {
- // The cart lives on the WC singleton, which the database rollback does not touch,
- // so empty it or the products some tests add leak into every later test.
- WC()->cart->empty_cart();
-
- WC()->countries->locale = null;
- $this->sut = null;
-
- if ( $this->checkout_phone_field_option_existed ) {
- update_option( 'woocommerce_checkout_phone_field', $this->checkout_phone_field_option_value );
- } else {
- delete_option( 'woocommerce_checkout_phone_field' );
- }
- } finally {
- parent::tearDown();
- }
- }
-
/**
* test_validate_existing_order_before_payment_valid_data.
*/
@@ -443,7 +404,6 @@ class OrderControllerTests extends \WC_Unit_Test_Case {
$threw = true;
} finally {
remove_action( 'woocommerce_before_calculate_totals', $thrower );
- WC()->cart->empty_cart();
}
$this->assertTrue( $threw, 'The injected exception should propagate out of create_order_from_cart().' );
@@ -466,12 +426,8 @@ class OrderControllerTests extends \WC_Unit_Test_Case {
WC()->cart->add_to_cart( $product->get_id() );
$this->assertFalse( WC()->cart->is_empty(), 'The cart must be non-empty so create_order_from_cart() runs to completion.' );
- try {
- $this->sut->create_order_from_cart();
- $this->sut->create_order_from_cart();
- } finally {
- WC()->cart->empty_cart();
- }
+ $this->sut->create_order_from_cart();
+ $this->sut->create_order_from_cart();
$this->assertSame(
$filters_before,
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/QuantityLimitsTests.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/QuantityLimitsTests.php
index f6594e1726e..7eeebc5b9b1 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/QuantityLimitsTests.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/QuantityLimitsTests.php
@@ -10,33 +10,6 @@ use Automattic\WooCommerce\StoreApi\Utilities\QuantityLimits;
* QuantityLimitsTests class.
*/
class QuantityLimitsTests extends \WC_Unit_Test_Case {
- /**
- * @var string
- */
- private $manage_stock;
-
- /**
- * Set up test environment.
- */
- public function setUp(): void {
- parent::setUp();
-
- $this->manage_stock = get_option( 'woocommerce_manage_stock' );
- }
-
- /**
- * Clean up test environment.
- */
- public function tearDown(): void {
- update_option( 'woocommerce_manage_stock', $this->manage_stock );
- // Clean up custom filters.
- remove_all_filters( 'woocommerce_store_api_product_quantity_multiple_of' );
- remove_all_filters( 'woocommerce_store_api_product_quantity_maximum' );
- remove_all_filters( 'woocommerce_store_api_product_quantity_minimum' );
- remove_all_filters( 'woocommerce_quantity_input_args' );
- parent::tearDown();
- }
-
/**
* Enable float support for tests.
*/
diff --git a/plugins/woocommerce/tests/php/src/Internal/AddressProvider/AbstractAutomatticAddressProviderTest.php b/plugins/woocommerce/tests/php/src/Internal/AddressProvider/AbstractAutomatticAddressProviderTest.php
index 5b90d47ea64..70aa1f70cba 100644
--- a/plugins/woocommerce/tests/php/src/Internal/AddressProvider/AbstractAutomatticAddressProviderTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/AddressProvider/AbstractAutomatticAddressProviderTest.php
@@ -70,20 +70,11 @@ class AbstractAutomatticAddressProviderTest extends \WC_Unit_Test_Case {
* Tear down test case.
*/
public function tearDown(): void {
- remove_all_filters( 'pre_update_option_woocommerce_address_autocomplete_enabled' );
- remove_all_filters( 'woocommerce_is_checkout' );
- remove_all_actions( 'wp_enqueue_scripts' );
- remove_filter( 'woocommerce_logging_class', array( $this, 'override_wc_logger' ) );
-
- // Dequeue and deregister scripts.
+ // The script registry lives on the $wp_scripts global, which the parent teardown
+ // does not reset, so dequeue and deregister explicitly.
wp_dequeue_script( 'a8c-address-autocomplete-service' );
wp_deregister_script( 'a8c-address-autocomplete-service' );
- // Clean up options.
- delete_option( 'test-provider_address_autocomplete_jwt' );
- delete_option( 'test-provider_jwt_retry_data' );
- delete_option( 'woocommerce_address_autocomplete_enabled' );
-
parent::tearDown();
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php
index 61cd4e474e3..a5db22b42eb 100644
--- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php
@@ -119,7 +119,6 @@ class OrdersTableDataStoreTests extends \HposTestCase {
remove_all_filters( 'wc_allow_changing_orders_storage_while_sync_is_pending' );
remove_all_filters( 'woocommerce_load_order_cogs_value' );
remove_all_filters( 'woocommerce_save_order_cogs_value' );
- wc()->cart->empty_cart();
parent::tearDown();
}
diff --git a/plugins/woocommerce/tests/php/src/UnitTestCaseTearDownTest.php b/plugins/woocommerce/tests/php/src/UnitTestCaseTearDownTest.php
new file mode 100644
index 00000000000..7675e069a98
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/UnitTestCaseTearDownTest.php
@@ -0,0 +1,73 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests;
+
+/**
+ * Tests that WC_Unit_Test_Case clears the WC() singleton state that neither the per-test
+ * database rollback nor the hook restore covers.
+ *
+ * This drives clear_wc_singleton_state() directly rather than dirtying state in one test and
+ * asserting it is gone in the next. A pair like that only holds under declaration order: run
+ * the suite with --order-by=random or reverse and the assertion half runs first, passes
+ * against state nothing has dirtied yet, and stops covering anything without ever going red.
+ */
+class UnitTestCaseTearDownTest extends \WC_Unit_Test_Case {
+
+ /**
+ * Synthetic locale value, so the leak assertion cannot be satisfied by a WooCommerce default.
+ */
+ private const LEAKED_LOCALE_LABEL = 'Leaked postcode label';
+
+ /**
+ * Every piece of singleton state the teardown is responsible for is cleared.
+ */
+ public function test_clear_wc_singleton_state_clears_what_survives_the_parent_teardown(): void {
+ $locale_filter = function ( $locale ) {
+ $locale['GB']['postcode']['label'] = self::LEAKED_LOCALE_LABEL;
+ return $locale;
+ };
+
+ add_filter(
+ 'woocommerce_get_country_locale',
+ $locale_filter
+ );
+
+ // Reading the locale under the filter is what caches the filtered value.
+ $locale = WC()->countries->get_country_locale();
+ $this->assertSame( self::LEAKED_LOCALE_LABEL, $locale['GB']['postcode']['label'], 'The filter should apply while it is attached.' );
+
+ WC()->cart->cart_context = 'store-api';
+
+ $product = \WC_Helper_Product::create_simple_product();
+ WC()->cart->add_to_cart( $product->get_id() );
+ $this->assertFalse( WC()->cart->is_empty(), 'The cart should hold an item.' );
+
+ wc_add_notice( 'Teardown coverage notice.' );
+ $this->assertSame( 1, wc_notice_count(), 'The notice should be queued.' );
+
+ $clear_persistent_cart = null;
+ $cart_emptied_callback = function ( $should_clear_persistent_cart ) use ( &$clear_persistent_cart ) {
+ $clear_persistent_cart = $should_clear_persistent_cart;
+ };
+ add_action( 'woocommerce_before_cart_emptied', $cart_emptied_callback );
+
+ // What tearDown() runs before handing off to the parent.
+ $this->clear_wc_singleton_state();
+ remove_action( 'woocommerce_before_cart_emptied', $cart_emptied_callback );
+
+ // The parent teardown restores the hooks straight afterwards, which is what leaves a
+ // filtered locale stranded in the cache. Drop the filter here to reproduce that order.
+ remove_filter( 'woocommerce_get_country_locale', $locale_filter );
+
+ $this->assertTrue( WC()->cart->is_empty(), 'The cart should have been emptied.' );
+ $this->assertSame( 'shortcode', WC()->cart->cart_context, 'The cart context should be back to shortcode.' );
+ $this->assertSame( 0, wc_notice_count(), 'The notice queue should have been cleared.' );
+ $this->assertFalse( $clear_persistent_cart, 'Teardown should leave persistent cart cleanup to the database rollback.' );
+ $this->assertNotSame(
+ self::LEAKED_LOCALE_LABEL,
+ WC()->countries->get_country_locale()['GB']['postcode']['label'] ?? null,
+ 'The filtered locale should not still be cached.'
+ );
+ }
+}