Commit 429e7cef5bf for woocommerce

commit 429e7cef5bf20e4ec5389b3e0d1138eea6dd60ed
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date:   Sat Aug 1 23:04:15 2026 +0300

    [tests] Fix filters registered in setUpBeforeClass being dropped (#67297)

    * fix: register test filters per test instead of in setUpBeforeClass

    WordPress snapshots the hook globals once per process, in the first
    `set_up()` that runs, and `_restore_hooks()` replays that snapshot after
    every test. A filter added from `setUpBeforeClass` is therefore added
    before the snapshot exists in some orderings and after it in others, and
    in the common case — any class that is not the first in the process — it
    is dropped as soon as the first test finishes.

    `BlockHooksTests` registered its `hooked_block_types` filter that way, by
    constructing the mock block in `setUpBeforeClass`. Only the first test
    ran with the filter in place. The two that follow assert with
    `assertNotContains` that the block is *not* hooked, and once the filter
    is gone they assert the absence of something that no longer exists at
    all: they passed no matter what the code did.

    That is measurable. Disabling the version gating in `BlockHooksTrait`
    entirely — so the block is hooked regardless of the configured version —
    left the class fully green when any other test class ran first, and only
    produced failures when `BlockHooksTests` happened to run first in the
    process and its filter landed inside the snapshot.

    `WC_Admin_Tests_Reports_Orders_Stats` has the same defect with
    `woocommerce_analytics_report_should_use_cache`. Its docblock says not to
    cache report data during these tests; in practice caching was disabled
    for the first test only, and the rest of the class ran against cached
    report data. A probe of `has_filter()` in the first and last test of the
    class reported the filter present and then absent.

    Both now register in `setUp()`, after `parent::setUp()`. Position
    matters: registering after the parent means the callback is never part of
    the snapshot, so it is added for every test and removed by the restore
    afterwards, in both orderings. That also makes the class-level
    `remove_filter` in `WC_Admin_Tests_Reports_Orders_Stats` redundant, so
    its `tearDownAfterClass` is gone.

    The mock block is still constructed once per class. Constructing it
    registers the `woocommerce/test-block` block type, and the block registry
    is not reset between tests, so doing it per test makes WordPress report
    "Block type is already registered" and the incorrect-usage catcher fails
    every test in the class. Only the filter needs to be per test.

    * test: isolate block hook fixtures per test

    The block hook tests shared a fixture created before WordPress captured its per-test hook snapshot. Later cases could therefore lose the constructor-registered filter while still reusing the trait's function-local version cache, making results depend on suite composition.

    Construct each scenario fixture during its test, use test-only subclasses for independent caches, and unregister the common mock block type during teardown. This keeps the tests deterministic without changing production code or defining suite-wide constants.

    * test: clarify reports cache lifecycle comment

    The existing explanation implied cached report values carried between test methods after WordPress restored the hook snapshot. Test teardown rolls those values back; the actual risk is that subsequent methods execute with report caching enabled.

    Describe that lifecycle precisely without changing test or production behavior.

diff --git a/plugins/woocommerce/changelog/fix-test-filters-registered-in-static-hooks b/plugins/woocommerce/changelog/fix-test-filters-registered-in-static-hooks
new file mode 100644
index 00000000000..ed43d4e1510
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-test-filters-registered-in-static-hooks
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Register test filters per test rather than in setUpBeforeClass, so they survive WordPress's per-test hook restore and the assertions that depend on them stop passing for the wrong reason.
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-orders-stats.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-orders-stats.php
index 5a1d4b063f9..84ff630f059 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-orders-stats.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/reports/class-wc-tests-reports-orders-stats.php
@@ -20,16 +20,13 @@ use Automattic\WooCommerce\Admin\Features\Fulfillments\Fulfillment;
  */
 class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
 	/**
-	 * Don't cache report data during these tests.
+	 * Set the database version and clear the fulfillment-status column flag for this class.
 	 */
 	public static function setUpBeforeClass(): void {
 		// Must come first: the parent reconnects `$wpdb`, which discards anything this
-		// method has written but not committed. The matching `parent::tearDownAfterClass()`
-		// goes last, so the two are deliberately not symmetrical.
+		// method has written but not committed.
 		parent::setUpBeforeClass();

-		add_filter( 'woocommerce_analytics_report_should_use_cache', '__return_false' );
-
 		$db_version = strstr( WC()->version, '-', true );
 		$db_version = $db_version ? $db_version : WC()->version;
 		update_option( 'woocommerce_db_version', $db_version );
@@ -38,12 +35,19 @@ class WC_Admin_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
 	}

 	/**
-	 * Restore cache for other tests.
+	 * Don't cache report data during these tests.
+	 *
+	 * This has to be registered per test, and after `parent::setUp()`. WordPress snapshots the
+	 * hook globals once per process and restores that snapshot after every test, so a filter
+	 * added from `setUpBeforeClass` is dropped once the first test finishes — leaving the rest
+	 * of the class running with report caching enabled, which is what these tests set out to avoid.
+	 * Registering it after the snapshot also means the restore removes it, so no class-level
+	 * teardown is needed to keep it away from other tests.
 	 */
-	public static function tearDownAfterClass(): void {
-		remove_filter( 'woocommerce_analytics_report_should_use_cache', '__return_false' );
+	public function setUp(): void {
+		parent::setUp();

-		parent::tearDownAfterClass();
+		add_filter( 'woocommerce_analytics_report_should_use_cache', '__return_false' );
 	}

 	/**
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/BlockHooksTests.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/BlockHooksTests.php
index 3f7167dd177..dca9c28cca6 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/BlockHooksTests.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/BlockHooksTests.php
@@ -4,20 +4,14 @@ declare( strict_types = 1 );
 namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes;

 use Automattic\WooCommerce\Tests\Blocks\Mocks\BlockHooksTestBlock;
+use Automattic\WooCommerce\Tests\Blocks\Mocks\BlockHooksLowerVersionTestBlock;
+use Automattic\WooCommerce\Tests\Blocks\Mocks\BlockHooksNoVersionTestBlock;
 use WP_UnitTestCase;

 /**
  * Tests Block Hooks logic.
- *
  */
 class BlockHooksTests extends WP_UnitTestCase {
-	/**
-	 * This variable holds our Product Query object.
-	 *
-	 * @var TestBlock
-	 */
-	protected static $block_instance;
-
 	/**
 	 * Option name for storing the block hooks version.
 	 *
@@ -26,21 +20,22 @@ class BlockHooksTests extends WP_UnitTestCase {
 	protected static $option_name = 'woocommerce_hooked_blocks_version';

 	/**
-	 * Initiate the mock object.
+	 * Clean up the mock block registration.
 	 */
-	public static function setUpBeforeClass(): void {
-		parent::setUpBeforeClass();
+	public function tearDown(): void {
+		$registry = \WP_Block_Type_Registry::get_instance();
+		if ( $registry->is_registered( 'woocommerce/test-block' ) ) {
+			unregister_block_type( 'woocommerce/test-block' );
+		}

-		delete_option( self::$option_name );
-		self::$block_instance = new BlockHooksTestBlock();
+		parent::tearDown();
 	}

 	/**
-	 * Test block gets hooked with correct version
-	 *
-	 * @return void
+	 * @testdox Should hook the mock block when the configured version meets the placement requirement.
 	 */
-	public function test_mocked_block_gets_hooked_with_correct_version() {
+	public function test_mocked_block_gets_hooked_with_correct_version(): void {
+		new BlockHooksTestBlock();
 		update_option( self::$option_name, '8.4.0', false );
 		// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- test code.
 		$hooked_block_types = apply_filters( 'hooked_block_types', array(), 'after', 'core/navigation', array( 'mock-context' ) );
@@ -53,11 +48,10 @@ class BlockHooksTests extends WP_UnitTestCase {
 	}

 	/**
-	 * Test block does not get hooked because no version is set.
-	 *
-	 * @return void
+	 * @testdox Should not hook the mock block when no version is configured.
 	 */
-	public function test_mocked_block_does_not_get_hooked() {
+	public function test_mocked_block_does_not_get_hooked(): void {
+		new BlockHooksNoVersionTestBlock();
 		delete_option( self::$option_name );
 		// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- test code.
 		$hooked_block_types = apply_filters( 'hooked_block_types', array(), 'after', 'core/navigation', array( 'mock-context' ) );
@@ -69,11 +63,10 @@ class BlockHooksTests extends WP_UnitTestCase {
 	}

 	/**
-	 * Test block does not get hooked with lower version
-	 *
-	 * @return void
+	 * @testdox Should not hook the mock block when the configured version is lower than required.
 	 */
-	public function test_mocked_block_does_not_get_hooked_with_lower_version() {
+	public function test_mocked_block_does_not_get_hooked_with_lower_version(): void {
+		new BlockHooksLowerVersionTestBlock();
 		update_option( self::$option_name, '8.3.0', false );
 		// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- test code.
 		$hooked_block_types = apply_filters( 'hooked_block_types', array(), 'after', 'core/navigation', array( 'mock-context' ) );
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Mocks/BlockHooksLowerVersionTestBlock.php b/plugins/woocommerce/tests/php/src/Blocks/Mocks/BlockHooksLowerVersionTestBlock.php
new file mode 100644
index 00000000000..a4a47b24576
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/Mocks/BlockHooksLowerVersionTestBlock.php
@@ -0,0 +1,13 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks\Mocks;
+
+use Automattic\WooCommerce\Blocks\Utils\BlockHooksTrait;
+
+/**
+ * Mock block with an independent version cache for the lower-version test.
+ */
+class BlockHooksLowerVersionTestBlock extends BlockHooksTestBlock {
+	use BlockHooksTrait;
+}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Mocks/BlockHooksNoVersionTestBlock.php b/plugins/woocommerce/tests/php/src/Blocks/Mocks/BlockHooksNoVersionTestBlock.php
new file mode 100644
index 00000000000..e3868adadc7
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/Mocks/BlockHooksNoVersionTestBlock.php
@@ -0,0 +1,13 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks\Mocks;
+
+use Automattic\WooCommerce\Blocks\Utils\BlockHooksTrait;
+
+/**
+ * Mock block with an independent version cache for the no-version test.
+ */
+class BlockHooksNoVersionTestBlock extends BlockHooksTestBlock {
+	use BlockHooksTrait;
+}