Skip to content

Upgrade PHPUnit to 11.x and reconcile with host unit-test tier (XWPENG-43) - #1974

Open
shadyvb wants to merge 3 commits into
ticket/XWPENG-42-unit-test-tierfrom
ticket/XWPENG-43-phpunit-11
Open

Upgrade PHPUnit to 11.x and reconcile with host unit-test tier (XWPENG-43)#1974
shadyvb wants to merge 3 commits into
ticket/XWPENG-42-unit-test-tierfrom
ticket/XWPENG-43-phpunit-11

Conversation

@shadyvb

@shadyvb shadyvb commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes XWPENG-43.

This PR upgrades the test toolchain to PHPUnit 11 and aligns it with the host unit suite introduced in XWPENG-42. Production refactors from XWPENG-42 are not part of the XWPENG-43 commit—they are already on this branch as prior commits (see below). Together, XWPENG-42 + XWPENG-43 form the combined stack on ticket/XWPENG-43-phpunit-11.

Summary

  • PHPUnit 9.6 → 11.5 with Solution Add special support for Settings Revisions #3 shim for expectDeprecated() on WP_StreamTestCase (PHPUnit 11 removed it; trait restores deprecated-expectation behavior where needed).
  • Reconciled with XWPENG-42: phpunit-unit.xml on PHPUnit 11 schema; Connectors_Unit_Test / Query_Unit_Test updated with #[DataProvider] attributes.
  • Test discovery: phpunit.xml / integration configs use suffix="Test.php"; ~58 integration test files renamed; removed WP_Stream_ prefix from class names (ability tests live in namespace WP_Stream, e.g. Ability_Create_Alert_Test).
  • Connector integration tests: removed Mock_Call_Recorder; expectations use split once()->with() (and named static record_log_call callbacks where multiple log entries must be matched in order).
  • Parity fixes (post-review): ACF save_post expectations; Mercator make_primary(); Users get_password_reset_key() multi-log scenarios via per-class static record_log_call helpers.
  • Multisite fix: ensure_mercator_mapping_table() in testcase—Admin ajax tests left stale mercator.db.version without creating the mapping table, polluting Mercator connector tests.
  • Cleanup: removed dead wp_stream_preload_coverage_sources() and tests/phpunit/class-mock-call-recorder.php.
  • Tooling: Composer config.platform 8.2, Docker PHP 8.2 default, switch-to:php7.4 build args, contributing.md corrections.
  • Filter_Input_Test: expectException() + #[WithoutErrorHandler] on the regex validation test.

Commits on this PR (develop..HEAD)

Commit Description
a5dbc77d XWPENG-42 — Host PHPUnit unit suite; Connectors load/register pipeline (production + unit tests).
c3c62d49 XWPENG-42 — Query SQL fragment refactor for unit testing (production + unit tests).
aeaf5d98 XWPENG-43 — PHPUnit 11 migration, test renames, connector expectation refactor, tooling, parity fixes.

NOT in the XWPENG-43 commit: Query/Connectors production refactors above—they ship on the same branch because the branch was built on top of XWPENG-42 work.

Breaking / acceptance criteria notes

  • Jira AC amended: PHP 8.2+ required for the test toolchain (not 8.0/8.1).
  • config.platform 8.2; plugin Requires PHP remains 7.2 (tracked separately as XWPENG-46).
  • Integration suites: relaxed failOnWarning / failOnNotice; scoped WithoutErrorHandler where WordPress emits expected noise.
  • No CI “423 guard” (declined for this effort).

Gotchas

  • Polyfills alias PHPUnit 4.0.0 as 1.1.5 for wp-test-utils compatibility.
  • @group annotations remain on integration tests.
  • EDD connector anonymous closure is pre-existing and unchanged.

Test plan

  • composer test-unit — 53 tests, exit 0
  • npm run test — 53 unit + 423 single-site + 423 multisite integration, all exit 0
  • composer lint and lint-tests pass
  • Spot-check connector tests (ACF, Mercator, Users) for ordered multi-log expectations after Mock_Call_Recorder removal

Checklist

  • Project documentation has been updated to reflect the changes in this pull request, if applicable.
  • I have tested the changes in the local development environment (see contributing.md).
  • I have added phpunit tests.

Release Changelog

  • Fix: N/A (developer tooling / test infrastructure).
  • New: N/A.

Release Checklist

  • This pull request is to the master branch.
  • Release version follows semantic versioning. Does it include breaking changes?
  • Update changelog in readme.txt.
  • Bump version in stream.php.
  • Bump Stable tag in readme.txt.
  • Bump version in classes/class-plugin.php.
  • Draft a release on GitHub.

@shadyvb
shadyvb force-pushed the ticket/XWPENG-43-phpunit-11 branch from 1481344 to aeaf5d9 Compare August 31, 2026 14:50
@shadyvb
shadyvb changed the base branch from develop to ticket/XWPENG-42-unit-test-tier September 1, 2026 09:19
@shadyvb
shadyvb force-pushed the ticket/XWPENG-43-phpunit-11 branch from fd317ee to 8cf6476 Compare September 1, 2026 10:46
@shadyvb
shadyvb requested review from PatelUtkarsh and bartoszgadomski and a balanced review from Copilot September 1, 2026 11:08

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Upgrades the test stack to PHPUnit 11/PHP 8.2 and modernizes integration-test discovery, mocks, and fixtures.

Changes:

  • Migrates PHPUnit configuration, data providers, and deprecated API shims.
  • Renames and restructures integration tests for suffix-based discovery.
  • Updates Docker, Composer, CI, and contributor tooling for PHP 8.2.

Review assessment:

  • High: the split editor tests can leak hooks because their teardown omits the parent teardown.
  • Medium: three settings tests no longer reject duplicate audit logs.

Reviewed changes

Copilot reviewed 79 out of 81 changed files in this pull request and generated no comments.

Show a summary per file
File Description
.github/workflows/lint-and-test.yml Uses PHP 8.2 in CI.
.gitignore Ignores PHPUnit 11 cache.
composer.json Upgrades PHPUnit and platform requirements.
contributing.md Documents PHP 8.2 testing requirements.
docker-compose.build.yml Updates PHP 8.2 image arguments.
local/docker/wordpress/Dockerfile Makes PHP 8.2/Xdebug 3.3 defaults.
package.json Adds explicit PHP 7.4 build arguments.
phpunit-multisite.xml Migrates multisite configuration to PHPUnit 11.
phpunit-unit.xml Migrates unit-suite configuration and discovery.
phpunit.xml Migrates integration configuration and discovery.
tests/bootstrap.php Loads renamed test base classes.
tests/phpcs.xml.dist Permits new test filenames.
tests/testcase.php Adds Mercator recovery and deprecation shim.
tests/phpunit/Abilities_Test.php Renames the test class.
tests/phpunit/Ability_Test.php Renames the test class.
tests/phpunit/Admin_Cron_Purge_Test.php Renames the test class and references.
tests/phpunit/Admin_Test.php Renames the test class.
tests/phpunit/Alert_Test.php Renames the test class.
tests/phpunit/Alert_Trigger_TestCase.php Renames the abstract alert base.
tests/phpunit/Alerts_List_Test.php Renames the test class.
tests/phpunit/Alerts_Test.php Renames the test class.
tests/phpunit/Author_Test.php Renames the test class.
tests/phpunit/CLI_Test.php Replaces an anonymous filter callback.
tests/phpunit/Connector_Test.php Renames the test class.
tests/phpunit/Connectors_Test.php Renames the test class.
tests/phpunit/Cron_Scheduler_Test.php Renames the test class.
tests/phpunit/DB_Driver_WPDB_Test.php Renames the test class.
tests/phpunit/DB_Test.php Renames the test class.
tests/phpunit/Date_Interval_Test.php Renames the test class.
tests/phpunit/Export_Test.php Renames the test class.
tests/phpunit/Exporter_CSV_Test.php Renames the test class.
tests/phpunit/Exporter_JSON_Test.php Renames the test class.
tests/phpunit/Filter_Input_Test.php Uses current exception and error-handler APIs.
tests/phpunit/Functions_Test.php Renames the test class.
tests/phpunit/Live_Update_Test.php Renames the test class.
tests/phpunit/Log_Test.php Renames the test class.
tests/phpunit/Network_Test.php Renames the test class and documentation.
tests/phpunit/Plugin_Test.php Renames the test class.
tests/phpunit/Scheduler_Handoff_Test.php Renames the test class.
tests/phpunit/Scheduler_Selection_Test.php Renames the test class.
tests/phpunit/fake-ability.php Updates the referenced test name.
tests/phpunit/abilities/Abilities_REST_Integration_Test.php Renames the class and updates expected-usage handling.
tests/phpunit/abilities/Ability_Create_Alert_Test.php Renames the test class.
tests/phpunit/abilities/Ability_Create_Exclusion_Rule_Test.php Renames the test class.
tests/phpunit/abilities/Ability_Delete_Alert_Test.php Renames the test class.
tests/phpunit/abilities/Ability_Get_Alerts_Test.php Renames the test class.
tests/phpunit/abilities/Ability_Get_Connectors_Test.php Renames the test class.
tests/phpunit/abilities/Ability_Get_Exclusion_Rules_Test.php Renames the test class.
tests/phpunit/abilities/Ability_Get_Record_Test.php Renames the test class.
tests/phpunit/abilities/Ability_Get_Records_Test.php Renames the test class.
tests/phpunit/abilities/Ability_Get_Settings_Test.php Renames the test class.
tests/phpunit/abilities/Ability_Purge_Records_Test.php Renames the test class.
tests/phpunit/abilities/Ability_Update_Settings_Test.php Renames the class and reference.
tests/phpunit/alerts/Alert_Trigger_Action_Test.php Uses the renamed alert base class.
tests/phpunit/alerts/Alert_Trigger_Author_Test.php Uses the renamed alert base class.
tests/phpunit/alerts/Alert_Trigger_Context_Test.php Uses the renamed alert base class.
tests/phpunit/connectors/Connector_ACF_Test.php Modernizes ordered ACF log assertions.
tests/phpunit/connectors/Connector_Blogs_Test.php Splits multisite connector scenarios.
tests/phpunit/connectors/Connector_Comments_Test.php Splits comment and reply scenarios.
tests/phpunit/connectors/Connector_EDD_Test.php Splits EDD logging scenarios.
tests/phpunit/connectors/Connector_Editor_Test.php Splits theme and plugin editor tests.
tests/phpunit/connectors/Connector_GravityForms_Test.php Recreates tests with PHPUnit 11 callbacks.
tests/phpunit/connectors/Connector_Installer_Test.php Replaces consecutive-call expectations.
tests/phpunit/connectors/Connector_Jetpack_Test.php Recreates and splits Jetpack tests.
tests/phpunit/connectors/Connector_Media_Test.php Splits attachment-parent scenarios.
tests/phpunit/connectors/Connector_Menus_Test.php Splits menu assignment scenarios.
tests/phpunit/connectors/Connector_Mercator_Test.php Modernizes ordered Mercator assertions.
tests/phpunit/connectors/Connector_Posts_Test.php Recreates and splits post-transition tests.
tests/phpunit/connectors/Connector_Settings_Test.php Splits settings and redaction scenarios.
tests/phpunit/connectors/Connector_Taxonomies_Test.php Modernizes taxonomy context assertions.
tests/phpunit/connectors/Connector_Two_Factor_Test.php Renames the test class.
tests/phpunit/connectors/Connector_User_Switching_Test.php Updates class and mock APIs.
tests/phpunit/connectors/Connector_Users_Test.php Splits and modernizes user log assertions.
tests/phpunit/connectors/Connector_WordPress_SEO_Test.php Renames the test class.
tests/phpunit/connectors/test-class-connector-blogs.php Removes the legacy Blogs test file.
tests/phpunit/connectors/test-class-connector-gravityforms.php Removes the legacy Gravity Forms test file.
tests/phpunit/connectors/test-class-connector-jetpack.php Removes the legacy Jetpack test file.
tests/phpunit/connectors/test-class-connector-posts.php Removes the legacy Posts test file.
tests/phpunit/unit/Connectors_Unit_Test.php Adds PHPUnit 11 data-provider attributes.
tests/phpunit/unit/Query_Unit_Test.php Adds PHPUnit 11 data-provider attributes.
Suppressed comments (4)

tests/phpunit/connectors/Connector_Editor_Test.php:89

  • Splitting this into a second test leaves the first test's mocked connector registered: this class's tearDown() restores the fixture file but never calls parent::tearDown(), so WordPress's hook snapshot is not restored. The second AJAX action can therefore invoke the previous mock's already-satisfied once() expectation and fail depending on execution order. Update the existing teardown to call parent::tearDown() after restoring the file.
    tests/phpunit/connectors/Connector_Settings_Test.php:107
  • The original combined test required exactly one log per option update; atLeastOnce() now allows duplicate audit records to pass unnoticed. Preserve that contract with once() for this single update.

This issue also appears in the following locations of the same file:

  • line 141
  • line 175
    tests/phpunit/connectors/Connector_Settings_Test.php:141
  • The original combined test required exactly one log per option update; atLeastOnce() now allows duplicate audit records to pass unnoticed. Preserve that contract with once() for this single update.
    tests/phpunit/connectors/Connector_Settings_Test.php:175
  • The original combined test required exactly one log per option update; atLeastOnce() now allows duplicate audit records to pass unnoticed. Preserve that contract with once() for this single update.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

…WPENG-43).

Migrate discovery, configs, and connector tests to PHPUnit 11; remove
Mock_Call_Recorder in favor of split once()->with() expectations; apply
parity fixes for ACF save_post, Mercator make_primary(), and Users
get_password_reset_key() multi-log scenarios via per-class static
record_log_call helpers; fix multisite Mercator test pollution and
strengthen log context assertions.
Align array and call indentation via PHPCBF, fix Yoda conditions, and correct assignment alignment for lint-tests.
Keep the connection-check coverage from #1966 discoverable after merging develop, and use a named filter callback instead of a closure.
@shadyvb
shadyvb force-pushed the ticket/XWPENG-43-phpunit-11 branch from 8cf6476 to 19d9307 Compare September 1, 2026 15:22

@bartoszgadomski bartoszgadomski left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shadyvb Thank you for working on this PR! Please check AI-assisted inline comments below.

@@ -44,61 +42,36 @@ public function tearDown(): void {
file_put_contents( WP_PLUGIN_DIR . '/hello.php', $this->original_contents ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tearDown() overrides the polyfills' tearDown() but never calls parent::tearDown(), so WP_Ajax_UnitTestCase::tear_down() and WP_UnitTestCase_Base::tear_down() never run for this class. For both tests here that means:

  • no ROLLBACK — the next test's start_transaction() issues START TRANSACTION, which implicitly commits whatever these tests wrote (the admin user, for one);
  • _restore_hooks() is skipped, so the mocked connector stays registered on wp_ajax_edit-theme-plugin-file;
  • wp_set_current_user( 0 ), the $_POST/$_GET reset and set_current_screen( 'front' ) are all skipped;
  • error_reporting() keeps the & ~E_WARNING mask that WP_Ajax_UnitTestCase::set_up() applies. Every later test saves that already-masked value in its own set_up() and restores it, so the mask sticks for the rest of the process.

It does pass today — PHPUnit drops the invocation mocker after each test in __phpunit_verify(), so the leftover mock's log() is an inert stub rather than a second once() violation — and the missing call predates this PR. But the class now has two tests sharing that state, so this is a good moment to fix it:

Suggested change
file_put_contents( WP_PLUGIN_DIR . '/hello.php', $this->original_contents ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
file_put_contents( WP_PLUGIN_DIR . '/hello.php', $this->original_contents ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
parent::tearDown();

call_user_func( $add_method, 'permalink_structure', '' );
$this->simulate_customize_save();

$this->mock->expects( $this->atLeastOnce() )

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test and the category_base / tag_base ones (lines 141 and 175) relax the count to atLeastOnce(), while test_callback_updated_option_users_can_register keeps once(). The combined test these were split from asserted exactly( 4 ) across four option writes, i.e. exactly one record per option, so once() should still hold here and would keep catching duplicate audit records.

If a single permalink/category/tag write genuinely logs more than once (a rewrite flush writing the option twice, say), a one-line comment saying so would help. Otherwise once() preserves the original contract.

Comment thread phpunit.xml
convertWarningsToExceptions="true"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
cacheDirectory=".phpunit.cache"
failOnWarning="false"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same run reports PHPUnit Deprecations: 87 and nothing surfaces them. displayDetailsOnPhpunitDeprecations="true" would make them visible. Most are probably the remaining doc-comment metadata — tests/ still has 23 @group and 7 @requires — and annotation metadata is removed in PHPUnit 12, so this is the tail of the same migration. The 7 @requires PHPUnit 5.7 are stale either way and can simply be deleted.

Comment thread tests/testcase.php
* @link https://core.trac.wordpress.org/ticket/62004
* @return void
*/
public function expectDeprecated(): void { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shim looks right and the docblock is honest about omitting annotation parsing. The residual risk is that it fails silently: @expectedDeprecated / @expectedIncorrectUsage on a future test will now be ignored rather than erroring, and the next person has no way to notice the annotation is inert.

Worth a line in contributing.md (or here) pointing at setExpectedDeprecated() / setExpectedIncorrectUsage() as the only supported form.

$this->assertFalse( 0 === did_action( $this->action_prefix . 'callback_user_register' ) );
}

public function test_callback_user_register_by_logged_in_user() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test creates TestGuy before expects() is configured, so the first New user registration call reaches the mock while it still has no matcher and only the second create is counted. That is what makes once() correct here, but the ordering is load-bearing and unmarked — moving expects() above the first create() breaks the test for a non-obvious reason.

A short comment noting that the first registration is intentionally unobserved would do it, or set the current user in setUp() so the test body only performs the observed create.

* @param mixed ...$args Log method arguments.
* @return void
*/
public static function record_log_call( ...$args ) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$recorded_log_calls and record_log_call() are duplicated verbatim in Connector_Users_Test and Connector_Mercator_Test. Since WP_StreamTestCase already gained shared helpers in this PR, a protected static recorder there (with the reset in its setUp()) would remove three copies and give the ordered-capture pattern one documented home.

*
* @var array
*/
private static $expected_post_context = array();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this property is declared after all the test methods, ~600 lines below the top of the class (same pattern in Connector_Settings_Test line 283 and in Connector_Media_Test), whereas the rest of the suite declares properties at the top with a docblock.

It's also not reset in setUp(), so a test that forgets to assign it would validate against the previous test's expectations instead of failing. Resetting it in setUp() would turn that into a hard failure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants