From c9898ab9a9dbe7e32814c42b4128db887f603b29 Mon Sep 17 00:00:00 2001
From: catch <catch@35733.no-reply.drupal.org>
Date: Mon, 14 Oct 2019 14:23:50 +0100
Subject: [PATCH] =?UTF-8?q?Issue=20#2950132=20by=20mondrake,=20alexpott,?=
 =?UTF-8?q?=20tom=5Fek,=20G=C3=A1bor=20Hojtsy,=20Pasqualle,=20andypost,=20?=
 =?UTF-8?q?Mile23,=20larowlan:=20Support=20PHPUnit=207=20optionally=20in?=
 =?UTF-8?q?=20Drupal=208,=20while=20keeping=20support=20for=20^6.5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 composer.json                                 |  4 +-
 composer.lock                                 | 18 +++---
 core/lib/Drupal/Core/Composer/Composer.php    | 42 ++++++++++++++
 core/lib/Drupal/Core/Test/TestDiscovery.php   |  1 +
 .../src/Kernel/Number/NumberItemTest.php      |  8 +--
 .../src/Functional/FileFieldTestBase.php      | 53 +++--------------
 .../src/Functional/FileFieldValidateTest.php  | 17 +++++-
 .../ContextualLinksTest.php                   |  2 +-
 .../src/Kernel/Migrate/d7/MigrateNodeTest.php |  2 +-
 core/scripts/run-tests.sh                     | 24 ++++++++
 .../Drupal/KernelTests/AssertLegacyTrait.php  | 34 ++---------
 .../Theme/ThemeRenderAndAutoescapeTest.php    |  2 +-
 .../Drupal/KernelTests/KernelTestBase.php     | 12 ----
 .../PhpUnit6/AfterSymfonyListener.php         | 24 ++++++++
 .../PhpUnit6/DrupalListener.php               | 40 +++++++++++++
 .../PhpUnit6/FileFieldTestBaseTrait.php       | 57 +++++++++++++++++++
 .../PhpUnit6/HtmlOutputPrinter.php            | 27 +++++++++
 .../PhpUnit6/SimpletestUiPrinter.php          | 17 ++++++
 .../PhpUnit6/StubTestSuiteBaseTrait.php       | 22 +++++++
 .../PhpUnit6/TestCompatibilityTrait.php       | 54 ++++++++++++++++++
 .../PhpUnit7/AfterSymfonyListener.php         | 24 ++++++++
 .../PhpUnit7/DrupalListener.php               | 40 +++++++++++++
 .../PhpUnit7/FileFieldTestBaseTrait.php       | 12 ++++
 .../PhpUnit7/HtmlOutputPrinter.php            | 27 +++++++++
 .../PhpUnit7/SimpletestUiPrinter.php          | 17 ++++++
 .../PhpUnit7/StubTestSuiteBaseTrait.php       | 22 +++++++
 .../PhpUnit7/TestCompatibilityTrait.php       | 54 ++++++++++++++++++
 .../PhpUnitCompatibility/RunnerVersion.php    | 30 ++++++++++
 core/tests/Drupal/Tests/BrowserTestBase.php   | 24 ++++----
 .../Tests/Core/Test/TestSuiteBaseTest.php     | 22 ++++---
 .../Tests/Listeners/AfterSymfonyListener.php  | 23 +++-----
 .../Drupal/Tests/Listeners/DrupalListener.php | 33 ++---------
 .../Tests/Listeners/HtmlOutputPrinter.php     | 31 ++--------
 .../Listeners/HtmlOutputPrinterTrait.php      | 25 ++++++++
 .../Tests/Listeners/SimpletestUiPrinter.php   | 25 +++-----
 .../Tests/PhpunitCompatibilityTrait.php       | 11 ++++
 .../Drupal/Tests/TestRequirementsTrait.php    | 14 ++++-
 core/tests/Drupal/Tests/UnitTestCase.php      |  2 +-
 core/tests/bootstrap.php                      |  1 +
 39 files changed, 680 insertions(+), 217 deletions(-)
 create mode 100644 core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit6/AfterSymfonyListener.php
 create mode 100644 core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit6/DrupalListener.php
 create mode 100644 core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit6/FileFieldTestBaseTrait.php
 create mode 100644 core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit6/HtmlOutputPrinter.php
 create mode 100644 core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit6/SimpletestUiPrinter.php
 create mode 100644 core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit6/StubTestSuiteBaseTrait.php
 create mode 100644 core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit6/TestCompatibilityTrait.php
 create mode 100644 core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit7/AfterSymfonyListener.php
 create mode 100644 core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit7/DrupalListener.php
 create mode 100644 core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit7/FileFieldTestBaseTrait.php
 create mode 100644 core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit7/HtmlOutputPrinter.php
 create mode 100644 core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit7/SimpletestUiPrinter.php
 create mode 100644 core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit7/StubTestSuiteBaseTrait.php
 create mode 100644 core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit7/TestCompatibilityTrait.php
 create mode 100644 core/tests/Drupal/TestTools/PhpUnitCompatibility/RunnerVersion.php

diff --git a/composer.json b/composer.json
index 0b52046f036c..1bd76c125e4b 100644
--- a/composer.json
+++ b/composer.json
@@ -18,7 +18,7 @@
         "jcalderonzumba/gastonjs": "^1.0.2",
         "jcalderonzumba/mink-phantomjs-driver": "^0.3.1",
         "mikey179/vfsstream": "^1.2",
-        "phpunit/phpunit": "^6.5",
+        "phpunit/phpunit": "^6.5 || ^7",
         "phpspec/prophecy": "^1.7",
         "symfony/css-selector": "^3.4.0",
         "symfony/phpunit-bridge": "^3.4.3",
@@ -73,6 +73,8 @@
         "pre-install-cmd": "Drupal\\Core\\Composer\\Composer::ensureComposerVersion",
         "pre-update-cmd": "Drupal\\Core\\Composer\\Composer::ensureComposerVersion",
         "pre-autoload-dump": "Drupal\\Core\\Composer\\Composer::preAutoloadDump",
+        "drupal-phpunit-upgrade-check": "Drupal\\Core\\Composer\\Composer::upgradePHPUnit",
+        "drupal-phpunit-upgrade": "@composer update phpunit/phpunit symfony/phpunit-bridge phpspec/prophecy symfony/yaml --with-dependencies --no-progress",
         "phpcs": "phpcs --standard=core/phpcs.xml.dist --runtime-set installed_paths $($COMPOSER_BINARY config vendor-dir)/drupal/coder/coder_sniffer --",
         "phpcbf": "phpcbf --standard=core/phpcs.xml.dist --runtime-set installed_paths $($COMPOSER_BINARY config vendor-dir)/drupal/coder/coder_sniffer --"
     },
diff --git a/composer.lock b/composer.lock
index 55d81156b7d6..0b54bbfb9a54 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "63b940ec40ef24930a101dfce6eed82a",
+    "content-hash": "8ba406bd7f3522d51f0e55fe33e51aff",
     "packages": [
         {
             "name": "asm89/stack-cors",
@@ -4726,8 +4726,8 @@
             "authors": [
                 {
                     "name": "Sebastian Bergmann",
-                    "role": "lead",
-                    "email": "sebastian@phpunit.de"
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
                 }
             ],
             "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
@@ -4774,8 +4774,8 @@
             "authors": [
                 {
                     "name": "Sebastian Bergmann",
-                    "role": "lead",
-                    "email": "sb@sebastian-bergmann.de"
+                    "email": "sb@sebastian-bergmann.de",
+                    "role": "lead"
                 }
             ],
             "description": "FilterIterator implementation that filters files based on a list of suffixes.",
@@ -4865,8 +4865,8 @@
             "authors": [
                 {
                     "name": "Sebastian Bergmann",
-                    "role": "lead",
-                    "email": "sb@sebastian-bergmann.de"
+                    "email": "sb@sebastian-bergmann.de",
+                    "role": "lead"
                 }
             ],
             "description": "Utility class for timing",
@@ -4996,8 +4996,8 @@
             "authors": [
                 {
                     "name": "Sebastian Bergmann",
-                    "role": "lead",
-                    "email": "sebastian@phpunit.de"
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
                 }
             ],
             "description": "The PHP Unit Testing framework.",
diff --git a/core/lib/Drupal/Core/Composer/Composer.php b/core/lib/Drupal/Core/Composer/Composer.php
index ed0303312fa8..627972c21c99 100644
--- a/core/lib/Drupal/Core/Composer/Composer.php
+++ b/core/lib/Drupal/Core/Composer/Composer.php
@@ -286,4 +286,46 @@ protected static function deleteRecursive($path) {
     return rmdir($path) && $success;
   }
 
+  /**
+   * Fires the drupal-phpunit-upgrade script event if necessary.
+   *
+   * @param \Composer\Script\Event $event
+   */
+  public static function upgradePHPUnit(Event $event) {
+    $repository = $event->getComposer()->getRepositoryManager()->getLocalRepository();
+    // This is, essentially, a null constraint. We only care whether the package
+    // is present in the vendor directory yet, but findPackage() requires it.
+    $constraint = new Constraint('>', '');
+    $phpunit_package = $repository->findPackage('phpunit/phpunit', $constraint);
+    if (!$phpunit_package) {
+      // There is nothing to do. The user is probably installing using the
+      // --no-dev flag.
+      return;
+    }
+
+    // If the PHP version is 7.3 or above and PHPUnit is less than version 7
+    // call the drupal-phpunit-upgrade script to upgrade PHPUnit.
+    if (!static::upgradePHPUnitCheck($phpunit_package->getVersion())) {
+      $event->getComposer()
+        ->getEventDispatcher()
+        ->dispatchScript('drupal-phpunit-upgrade');
+    }
+  }
+
+  /**
+   * Determines if PHPUnit needs to be upgraded.
+   *
+   * This method is located in this file because it is possible that it is
+   * called before the autoloader is available.
+   *
+   * @param string $phpunit_version
+   *   The PHPUnit version string.
+   *
+   * @return bool
+   *   TRUE if the PHPUnit needs to be upgraded, FALSE if not.
+   */
+  public static function upgradePHPUnitCheck($phpunit_version) {
+    return !(version_compare(PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION, '7.3') >= 0 && version_compare($phpunit_version, '7.0') < 0);
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Test/TestDiscovery.php b/core/lib/Drupal/Core/Test/TestDiscovery.php
index 552a72acf9e9..7c6242f950ae 100644
--- a/core/lib/Drupal/Core/Test/TestDiscovery.php
+++ b/core/lib/Drupal/Core/Test/TestDiscovery.php
@@ -85,6 +85,7 @@ public function registerTestNamespaces() {
     $this->testNamespaces['Drupal\\KernelTests\\'] = [$this->root . '/core/tests/Drupal/KernelTests'];
     $this->testNamespaces['Drupal\\FunctionalTests\\'] = [$this->root . '/core/tests/Drupal/FunctionalTests'];
     $this->testNamespaces['Drupal\\FunctionalJavascriptTests\\'] = [$this->root . '/core/tests/Drupal/FunctionalJavascriptTests'];
+    $this->testNamespaces['Drupal\\TestTools\\'] = [$this->root . '/core/tests/Drupal/TestTools'];
 
     $this->availableExtensions = [];
     foreach ($this->getExtensions() as $name => $extension) {
diff --git a/core/modules/field/tests/src/Kernel/Number/NumberItemTest.php b/core/modules/field/tests/src/Kernel/Number/NumberItemTest.php
index ba47d91307ce..f9a82a4d546b 100644
--- a/core/modules/field/tests/src/Kernel/Number/NumberItemTest.php
+++ b/core/modules/field/tests/src/Kernel/Number/NumberItemTest.php
@@ -72,8 +72,8 @@ public function testNumberItem() {
     $this->assertEqual($entity->field_float[0]->value, $float);
     $this->assertTrue($entity->field_decimal instanceof FieldItemListInterface, 'Field implements interface.');
     $this->assertTrue($entity->field_decimal[0] instanceof FieldItemInterface, 'Field item implements interface.');
-    $this->assertEqual($entity->field_decimal->value, $decimal);
-    $this->assertEqual($entity->field_decimal[0]->value, $decimal);
+    $this->assertEqual($entity->field_decimal->value, (float) $decimal);
+    $this->assertEqual($entity->field_decimal[0]->value, (float) $decimal);
 
     // Verify changing the number value.
     $new_integer = rand(11, 20);
@@ -84,14 +84,14 @@ public function testNumberItem() {
     $entity->field_float->value = $new_float;
     $this->assertEqual($entity->field_float->value, $new_float);
     $entity->field_decimal->value = $new_decimal;
-    $this->assertEqual($entity->field_decimal->value, $new_decimal);
+    $this->assertEqual($entity->field_decimal->value, (float) $new_decimal);
 
     // Read changed entity and assert changed values.
     $entity->save();
     $entity = EntityTest::load($id);
     $this->assertEqual($entity->field_integer->value, $new_integer);
     $this->assertEqual($entity->field_float->value, $new_float);
-    $this->assertEqual($entity->field_decimal->value, $new_decimal);
+    $this->assertEqual($entity->field_decimal->value, (float) $new_decimal);
 
     // Test sample item generation.
     $entity = EntityTest::create();
diff --git a/core/modules/file/tests/src/Functional/FileFieldTestBase.php b/core/modules/file/tests/src/Functional/FileFieldTestBase.php
index d738c9ab72a5..fc57f1747fdf 100644
--- a/core/modules/file/tests/src/Functional/FileFieldTestBase.php
+++ b/core/modules/file/tests/src/Functional/FileFieldTestBase.php
@@ -3,6 +3,7 @@
 namespace Drupal\Tests\file\Functional;
 
 use Drupal\Component\Render\FormattableMarkup;
+use Drupal\TestTools\PhpUnitCompatibility\RunnerVersion;
 use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\file\FileInterface;
@@ -10,6 +11,13 @@
 use Drupal\file\Entity\File;
 use Drupal\Tests\TestFileCreationTrait;
 
+// In order to manage different method signatures between PHPUnit versions, we
+// dynamically load a compatibility trait dependent on the PHPUnit runner
+// version.
+if (!trait_exists(PhpunitVersionDependentFileFieldTestBaseTrait::class, FALSE)) {
+  class_alias("Drupal\TestTools\PhpUnitCompatibility\PhpUnit" . RunnerVersion::getMajor() . "\FileFieldTestBaseTrait", PhpunitVersionDependentFileFieldTestBaseTrait::class);
+}
+
 /**
  * Provides methods specifically for testing File module's field handling.
  */
@@ -19,6 +27,7 @@ abstract class FileFieldTestBase extends BrowserTestBase {
   use TestFileCreationTrait {
     getTestFiles as drupalGetTestFiles;
   }
+  use PhpunitVersionDependentFileFieldTestBaseTrait;
 
   /**
    * {@inheritdoc}
@@ -200,28 +209,6 @@ public function replaceNodeFile($file, $field_name, $nid, $new_revision = TRUE)
     $this->drupalPostForm(NULL, $edit, t('Save'));
   }
 
-  /**
-   * Asserts that a file exists physically on disk.
-   *
-   * Overrides PHPUnit\Framework\Assert::assertFileExists() to also work with
-   * file entities.
-   *
-   * @param \Drupal\File\FileInterface|string $file
-   *   Either the file entity or the file URI.
-   * @param string $message
-   *   (optional) A message to display with the assertion.
-   *
-   * @see https://www.drupal.org/node/3057326
-   */
-  public static function assertFileExists($file, $message = NULL) {
-    if ($file instanceof FileInterface) {
-      @trigger_error('Passing a File entity as $file argument to FileFieldTestBase::assertFileExists is deprecated in drupal:8.8.0. It will be removed from drupal:9.0.0. Instead, pass the File entity URI via File::getFileUri(). See https://www.drupal.org/node/3057326', E_USER_DEPRECATED);
-      $file = $file->getFileUri();
-    }
-    $message = isset($message) ? $message : new FormattableMarkup('File %file exists on the disk.', ['%file' => $file]);
-    parent::assertFileExists($file, $message);
-  }
-
   /**
    * Asserts that a file exists in the database.
    */
@@ -232,28 +219,6 @@ public function assertFileEntryExists($file, $message = NULL) {
     $this->assertEqual($db_file->getFileUri(), $file->getFileUri(), $message);
   }
 
-  /**
-   * Asserts that a file does not exist on disk.
-   *
-   * Overrides PHPUnit\Framework\Assert::assertFileNotExists() to also work
-   * with file entities.
-   *
-   * @param \Drupal\File\FileInterface|string $file
-   *   Either the file entity or the file URI.
-   * @param string $message
-   *   (optional) A message to display with the assertion.
-   *
-   * @see https://www.drupal.org/node/3057326
-   */
-  public static function assertFileNotExists($file, $message = NULL) {
-    if ($file instanceof FileInterface) {
-      @trigger_error('Passing a File entity as $file argument to FileFieldTestBase::assertFileNotExists is deprecated in drupal:8.8.0. It will be removed from drupal:9.0.0. Instead, pass the File entity URI via File::getFileUri(). See https://www.drupal.org/node/3057326', E_USER_DEPRECATED);
-      $file = $file->getFileUri();
-    }
-    $message = isset($message) ? $message : new FormattableMarkup('File %file exists on the disk.', ['%file' => $file]);
-    parent::assertFileNotExists($file, $message);
-  }
-
   /**
    * Asserts that a file does not exist in the database.
    */
diff --git a/core/modules/file/tests/src/Functional/FileFieldValidateTest.php b/core/modules/file/tests/src/Functional/FileFieldValidateTest.php
index ec7b99d63df9..5c7b5cfff1a3 100644
--- a/core/modules/file/tests/src/Functional/FileFieldValidateTest.php
+++ b/core/modules/file/tests/src/Functional/FileFieldValidateTest.php
@@ -4,8 +4,10 @@
 
 use Drupal\Component\Render\FormattableMarkup;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\TestTools\PhpUnitCompatibility\RunnerVersion;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\file\Entity\File;
+use Drupal\Tests\Traits\ExpectDeprecationTrait;
 
 /**
  * Tests validation functions such as file type, max file size, max size per
@@ -15,6 +17,8 @@
  */
 class FileFieldValidateTest extends FileFieldTestBase {
 
+  use ExpectDeprecationTrait;
+
   /**
    * Tests the required property on file fields.
    */
@@ -193,10 +197,19 @@ public function testFileRemoval() {
    *
    * @group legacy
    *
-   * @expectedDeprecation Passing a File entity as $file argument to FileFieldTestBase::assertFileExists is deprecated in drupal:8.8.0. It will be removed from drupal:9.0.0. Instead, pass the File entity URI via File::getFileUri(). See https://www.drupal.org/node/3057326
-   * @expectedDeprecation Passing a File entity as $file argument to FileFieldTestBase::assertFileNotExists is deprecated in drupal:8.8.0. It will be removed from drupal:9.0.0. Instead, pass the File entity URI via File::getFileUri(). See https://www.drupal.org/node/3057326
+   * @todo the expectedDeprecation annotation does not work if tests are marked
+   *   skipped.
+   * @see https://github.com/symfony/symfony/pull/25757
    */
   public function testAssertFileExistsDeprecation() {
+    if (RunnerVersion::getMajor() == 6) {
+      $this->expectDeprecation('Passing a File entity as $file argument to FileFieldTestBase::assertFileExists is deprecated in drupal:8.8.0. It will be removed from drupal:9.0.0. Instead, pass the File entity URI via File::getFileUri(). See https://www.drupal.org/node/3057326');
+      $this->expectDeprecation('Passing a File entity as $file argument to FileFieldTestBase::assertFileNotExists is deprecated in drupal:8.8.0. It will be removed from drupal:9.0.0. Instead, pass the File entity URI via File::getFileUri(). See https://www.drupal.org/node/3057326');
+    }
+    else {
+      $this->markTestSkipped('This test does not work in PHPUnit 7+ since assertFileExists only accepts string arguments for $file');
+    }
+
     $node_storage = $this->container->get('entity.manager')->getStorage('node');
     $type_name = 'article';
     $field_name = 'file_test';
diff --git a/core/modules/node/tests/src/FunctionalJavascript/ContextualLinksTest.php b/core/modules/node/tests/src/FunctionalJavascript/ContextualLinksTest.php
index 98051262ec28..7e9dae071b8e 100644
--- a/core/modules/node/tests/src/FunctionalJavascript/ContextualLinksTest.php
+++ b/core/modules/node/tests/src/FunctionalJavascript/ContextualLinksTest.php
@@ -111,7 +111,7 @@ public function testRevisionContextualLinks() {
 
     $this->toggleContextualTriggerVisibility('main');
     $contextual_button = $page->find('css', 'main .contextual button');
-    $this->assertEmpty(0, $contextual_button);
+    $this->assertEmpty(0, $contextual_button ?: '');
   }
 
 }
diff --git a/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeTest.php b/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeTest.php
index 36fd9b0b42a3..6915e4dbe4f4 100644
--- a/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeTest.php
+++ b/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeTest.php
@@ -158,7 +158,7 @@ public function testNode() {
     $this->assertSame('2015-01-20T04:15:00', $node->field_date->value);
     $this->assertSame('2015-01-20', $node->field_date_without_time->value);
     $this->assertSame('2015-01-20', $node->field_datetime_without_time->value);
-    $this->assertEquals('1', $node->field_float->value);
+    $this->assertEquals(1, $node->field_float->value);
     $this->assertEquals('5', $node->field_integer->value);
     $this->assertEquals('Some more text', $node->field_text_list[0]->value);
     $this->assertEquals('7', $node->field_integer_list[0]->value);
diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh
index 39ae5ff69f2c..528b39f9c612 100755
--- a/core/scripts/run-tests.sh
+++ b/core/scripts/run-tests.sh
@@ -8,6 +8,7 @@
 use Drupal\Component\FileSystem\FileSystem;
 use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\Timer;
+use Drupal\Core\Composer\Composer;
 use Drupal\Core\Database\Database;
 use Drupal\Core\File\Exception\FileException;
 use Drupal\Core\Test\EnvironmentCleaner;
@@ -17,6 +18,7 @@
 use Drupal\Core\Test\TestRunnerKernel;
 use Drupal\Core\Test\TestDiscovery;
 use PHPUnit\Framework\TestCase;
+use PHPUnit\Runner\Version;
 use Symfony\Component\Console\Output\ConsoleOutput;
 use Symfony\Component\Filesystem\Filesystem as SymfonyFilesystem;
 use Symfony\Component\HttpFoundation\Request;
@@ -149,6 +151,11 @@
   exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
 }
 
+if (!Composer::upgradePHPUnitCheck(Version::id())) {
+  simpletest_script_print_error("PHPUnit testing framework version 7 or greater is required when running on PHP 7.3 or greater. Run the command 'composer run-script drupal-phpunit-upgrade' in order to fix this.");
+  exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
+}
+
 $test_list = simpletest_script_get_test_list();
 
 // Try to allocate unlimited time to run the tests.
@@ -483,7 +490,24 @@ function simpletest_script_init() {
     exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
   }
 
+  // Detect if we're in the top-level process using the private 'execute-test'
+  // argument. Determine if being run on drupal.org's testing infrastructure
+  // using the presence of 'drupalci' in the sqlite argument.
+  // @todo https://www.drupal.org/project/drupalci_testbot/issues/2860941 Use
+  //   better environment variable to detect DrupalCI.
+  if (!$args['execute-test'] && preg_match('/drupalci/', $args['sqlite'])) {
+    // Update PHPUnit if needed and possible. There is a later check once the
+    // autoloader is in place to ensure we're on the correct version. We need to
+    // do this before the autoloader is in place to ensure that it is correct.
+    $composer = ($composer = rtrim('\\' === DIRECTORY_SEPARATOR ? preg_replace('/[\r\n].*/', '', `where.exe composer.phar`) : `which composer.phar`))
+      ? $php . ' ' . escapeshellarg($composer)
+      : 'composer';
+    passthru("$composer run-script drupal-phpunit-upgrade-check");
+  }
+
   $autoloader = require_once __DIR__ . '/../../autoload.php';
+  // The PHPUnit compatibility layer needs to be available to autoload tests.
+  $autoloader->add('Drupal\\TestTools', __DIR__ . '/../tests');
 
   // Get URL from arguments.
   if (!empty($args['url'])) {
diff --git a/core/tests/Drupal/KernelTests/AssertLegacyTrait.php b/core/tests/Drupal/KernelTests/AssertLegacyTrait.php
index 8a0fd574b2cf..1360798e4618 100644
--- a/core/tests/Drupal/KernelTests/AssertLegacyTrait.php
+++ b/core/tests/Drupal/KernelTests/AssertLegacyTrait.php
@@ -27,30 +27,6 @@ protected function assert($actual, $message = '') {
     parent::assertTrue((bool) $actual, $message);
   }
 
-  /**
-   * @see \Drupal\simpletest\TestBase::assertTrue()
-   */
-  public static function assertTrue($actual, $message = '') {
-    if (is_bool($actual)) {
-      parent::assertTrue($actual, $message);
-    }
-    else {
-      parent::assertNotEmpty($actual, $message);
-    }
-  }
-
-  /**
-   * @see \Drupal\simpletest\TestBase::assertFalse()
-   */
-  public static function assertFalse($actual, $message = '') {
-    if (is_bool($actual)) {
-      parent::assertFalse($actual, $message);
-    }
-    else {
-      parent::assertEmpty($actual, $message);
-    }
-  }
-
   /**
    * @see \Drupal\simpletest\TestBase::assertEqual()
    *
@@ -58,7 +34,7 @@ public static function assertFalse($actual, $message = '') {
    *   instead.
    */
   protected function assertEqual($actual, $expected, $message = '') {
-    $this->assertEquals($expected, $actual, $message);
+    $this->assertEquals($expected, $actual, (string) $message);
   }
 
   /**
@@ -68,7 +44,7 @@ protected function assertEqual($actual, $expected, $message = '') {
    *   self::assertNotEquals() instead.
    */
   protected function assertNotEqual($actual, $expected, $message = '') {
-    $this->assertNotEquals($expected, $actual, $message);
+    $this->assertNotEquals($expected, $actual, (string) $message);
   }
 
   /**
@@ -78,7 +54,7 @@ protected function assertNotEqual($actual, $expected, $message = '') {
    *   instead.
    */
   protected function assertIdentical($actual, $expected, $message = '') {
-    $this->assertSame($expected, $actual, $message);
+    $this->assertSame($expected, $actual, (string) $message);
   }
 
   /**
@@ -88,7 +64,7 @@ protected function assertIdentical($actual, $expected, $message = '') {
    *   self::assertNotSame() instead.
    */
   protected function assertNotIdentical($actual, $expected, $message = '') {
-    $this->assertNotSame($expected, $actual, $message);
+    $this->assertNotSame($expected, $actual, (string) $message);
   }
 
   /**
@@ -101,7 +77,7 @@ protected function assertIdenticalObject($actual, $expected, $message = '') {
     // Note: ::assertSame checks whether its the same object. ::assertEquals
     // though compares
 
-    $this->assertEquals($expected, $actual, $message);
+    $this->assertEquals($expected, $actual, (string) $message);
   }
 
   /**
diff --git a/core/tests/Drupal/KernelTests/Core/Theme/ThemeRenderAndAutoescapeTest.php b/core/tests/Drupal/KernelTests/Core/Theme/ThemeRenderAndAutoescapeTest.php
index 8d51d254375a..058669b590a9 100644
--- a/core/tests/Drupal/KernelTests/Core/Theme/ThemeRenderAndAutoescapeTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Theme/ThemeRenderAndAutoescapeTest.php
@@ -64,7 +64,7 @@ public function providerTestThemeRenderAndAutoescape() {
       'empty string unchanged' => ['', ''],
       'simple string unchanged' => ['ab', 'ab'],
       'int (scalar) cast to string' => [111, '111'],
-      'float (scalar) cast to string' => [2.10, '2.10'],
+      'float (scalar) cast to string' => [2.10, '2.1'],
       '> is escaped' => ['>', '&gt;'],
       'Markup EM tag is unchanged' => [Markup::create('<em>hi</em>'), '<em>hi</em>'],
       'Markup SCRIPT tag is unchanged' => [Markup::create('<script>alert("hi");</script>'), '<script>alert("hi");</script>'],
diff --git a/core/tests/Drupal/KernelTests/KernelTestBase.php b/core/tests/Drupal/KernelTests/KernelTestBase.php
index ad46657cf646..dfda4a9fc060 100644
--- a/core/tests/Drupal/KernelTests/KernelTestBase.php
+++ b/core/tests/Drupal/KernelTests/KernelTestBase.php
@@ -1096,16 +1096,4 @@ public function __sleep() {
     return [];
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public static function assertEquals($expected, $actual, $message = '', $delta = 0.0, $maxDepth = 10, $canonicalize = FALSE, $ignoreCase = FALSE) {
-    // Cast objects implementing MarkupInterface to string instead of
-    // relying on PHP casting them to string depending on what they are being
-    // comparing with.
-    $expected = static::castSafeStrings($expected);
-    $actual = static::castSafeStrings($actual);
-    parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
-  }
-
 }
diff --git a/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit6/AfterSymfonyListener.php b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit6/AfterSymfonyListener.php
new file mode 100644
index 000000000000..92732557f18e
--- /dev/null
+++ b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit6/AfterSymfonyListener.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\TestTools\PhpUnitCompatibility\PhpUnit6;
+
+use PHPUnit\Framework\Test;
+use PHPUnit\Framework\TestListener;
+use PHPUnit\Framework\TestListenerDefaultImplementation;
+
+/**
+ * Listens to PHPUnit test runs.
+ *
+ * @internal
+ */
+class AfterSymfonyListener implements TestListener {
+  use TestListenerDefaultImplementation;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function endTest(Test $test, $time) {
+    restore_error_handler();
+  }
+
+}
diff --git a/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit6/DrupalListener.php b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit6/DrupalListener.php
new file mode 100644
index 000000000000..4cb66a1797ee
--- /dev/null
+++ b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit6/DrupalListener.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Drupal\TestTools\PhpUnitCompatibility\PhpUnit6;
+
+use Drupal\Tests\Listeners\DeprecationListenerTrait;
+use Drupal\Tests\Listeners\DrupalComponentTestListenerTrait;
+use Drupal\Tests\Listeners\DrupalStandardsListenerTrait;
+use PHPUnit\Framework\TestListener;
+use PHPUnit\Framework\TestListenerDefaultImplementation;
+use PHPUnit\Framework\Test;
+
+/**
+ * Listens to PHPUnit test runs.
+ *
+ * @internal
+ */
+class DrupalListener implements TestListener {
+
+  use TestListenerDefaultImplementation;
+  use DeprecationListenerTrait;
+  use DrupalComponentTestListenerTrait;
+  use DrupalStandardsListenerTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function startTest(Test $test) {
+    $this->deprecationStartTest($test);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function endTest(Test $test, $time) {
+    $this->deprecationEndTest($test, $time);
+    $this->componentEndTest($test, $time);
+    $this->standardsEndTest($test, $time);
+  }
+
+}
diff --git a/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit6/FileFieldTestBaseTrait.php b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit6/FileFieldTestBaseTrait.php
new file mode 100644
index 000000000000..212485f90ca2
--- /dev/null
+++ b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit6/FileFieldTestBaseTrait.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Drupal\TestTools\PhpUnitCompatibility\PhpUnit6;
+
+use Drupal\Component\Render\FormattableMarkup;
+use Drupal\file\FileInterface;
+
+/**
+ * Makes Drupal's test API forward compatible with multiple versions of PHPUnit.
+ */
+trait FileFieldTestBaseTrait {
+
+  /**
+   * Asserts that a file exists physically on disk.
+   *
+   * Overrides PHPUnit\Framework\Assert::assertFileExists() to also work with
+   * file entities.
+   *
+   * @param \Drupal\File\FileInterface|string $file
+   *   Either the file entity or the file URI.
+   * @param string $message
+   *   (optional) A message to display with the assertion.
+   *
+   * @see https://www.drupal.org/node/3057326
+   */
+  public static function assertFileExists($file, $message = NULL) {
+    if ($file instanceof FileInterface) {
+      @trigger_error('Passing a File entity as $file argument to FileFieldTestBase::assertFileExists is deprecated in drupal:8.8.0. It will be removed from drupal:9.0.0. Instead, pass the File entity URI via File::getFileUri(). See https://www.drupal.org/node/3057326', E_USER_DEPRECATED);
+      $file = $file->getFileUri();
+    }
+    $message = isset($message) ? $message : new FormattableMarkup('File %file exists on the disk.', ['%file' => $file]);
+    parent::assertFileExists($file, $message);
+  }
+
+  /**
+   * Asserts that a file does not exist on disk.
+   *
+   * Overrides PHPUnit\Framework\Assert::assertFileNotExists() to also work
+   * with file entities.
+   *
+   * @param \Drupal\File\FileInterface|string $file
+   *   Either the file entity or the file URI.
+   * @param string $message
+   *   (optional) A message to display with the assertion.
+   *
+   * @see https://www.drupal.org/node/3057326
+   */
+  public static function assertFileNotExists($file, $message = NULL) {
+    if ($file instanceof FileInterface) {
+      @trigger_error('Passing a File entity as $file argument to FileFieldTestBase::assertFileNotExists is deprecated in drupal:8.8.0. It will be removed from drupal:9.0.0. Instead, pass the File entity URI via File::getFileUri(). See https://www.drupal.org/node/3057326', E_USER_DEPRECATED);
+      $file = $file->getFileUri();
+    }
+    $message = isset($message) ? $message : new FormattableMarkup('File %file exists on the disk.', ['%file' => $file]);
+    parent::assertFileNotExists($file, $message);
+  }
+
+}
diff --git a/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit6/HtmlOutputPrinter.php b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit6/HtmlOutputPrinter.php
new file mode 100644
index 000000000000..c3a6a1821f1b
--- /dev/null
+++ b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit6/HtmlOutputPrinter.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\TestTools\PhpUnitCompatibility\PhpUnit6;
+
+use Drupal\Tests\Listeners\HtmlOutputPrinterTrait;
+use PHPUnit\Framework\TestResult;
+use PHPUnit\TextUI\ResultPrinter;
+
+/**
+ * Defines a class for providing html output results for functional tests.
+ *
+ * @internal
+ */
+class HtmlOutputPrinter extends ResultPrinter {
+
+  use HtmlOutputPrinterTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function printResult(TestResult $result) {
+    parent::printResult($result);
+
+    $this->printHtmlOutput();
+  }
+
+}
diff --git a/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit6/SimpletestUiPrinter.php b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit6/SimpletestUiPrinter.php
new file mode 100644
index 000000000000..5ea80f48e4c5
--- /dev/null
+++ b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit6/SimpletestUiPrinter.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Drupal\TestTools\PhpUnitCompatibility\PhpUnit6;
+
+/**
+ * Defines a class for providing html output links in the Simpletest UI.
+ */
+class SimpletestUiPrinter extends HtmlOutputPrinter {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function write($buffer) {
+    $this->simpletestUiWrite($buffer);
+  }
+
+}
diff --git a/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit6/StubTestSuiteBaseTrait.php b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit6/StubTestSuiteBaseTrait.php
new file mode 100644
index 000000000000..80ae1901c2ab
--- /dev/null
+++ b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit6/StubTestSuiteBaseTrait.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Drupal\TestTools\PhpUnitCompatibility\PhpUnit6;
+
+/**
+ * Makes Drupal's test API forward compatible with multiple versions of PHPUnit.
+ */
+trait StubTestSuiteBaseTrait {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addTestFiles($filenames) {
+    // We stub addTestFiles() because the parent implementation can't deal with
+    // vfsStream-based filesystems due to an error in
+    // stream_resolve_include_path(). See
+    // https://github.com/mikey179/vfsStream/issues/5 Here we just store the
+    // test file being added in $this->testFiles.
+    $this->testFiles = array_merge($this->testFiles, $filenames);
+  }
+
+}
diff --git a/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit6/TestCompatibilityTrait.php b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit6/TestCompatibilityTrait.php
new file mode 100644
index 000000000000..0785472243c3
--- /dev/null
+++ b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit6/TestCompatibilityTrait.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\TestTools\PhpUnitCompatibility\PhpUnit6;
+
+/**
+ * Makes Drupal's test API forward compatible with multiple versions of PHPUnit.
+ */
+trait TestCompatibilityTrait {
+
+  /**
+   * @todo deprecate this method override in
+   *   https://www.drupal.org/project/drupal/issues/2742585
+   *
+   * @see \Drupal\simpletest\TestBase::assertTrue()
+   */
+  public static function assertTrue($actual, $message = '') {
+    if (is_bool($actual)) {
+      parent::assertTrue($actual, $message);
+    }
+    else {
+      parent::assertNotEmpty($actual, $message);
+    }
+  }
+
+  /**
+   * @todo deprecate this method override in
+   *   https://www.drupal.org/project/drupal/issues/2742585
+   *
+   * @see \Drupal\simpletest\TestBase::assertFalse()
+   */
+  public static function assertFalse($actual, $message = '') {
+    if (is_bool($actual)) {
+      parent::assertFalse($actual, $message);
+    }
+    else {
+      parent::assertEmpty($actual, $message);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function assertEquals($expected, $actual, $message = '', $delta = 0, $maxDepth = 10, $canonicalize = FALSE, $ignoreCase = FALSE) {
+    // Cast objects implementing MarkupInterface to string instead of
+    // relying on PHP casting them to string depending on what they are being
+    // comparing with.
+    if (method_exists(self::class, 'castSafeStrings')) {
+      $expected = self::castSafeStrings($expected);
+      $actual = self::castSafeStrings($actual);
+    }
+    parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
+  }
+
+}
diff --git a/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit7/AfterSymfonyListener.php b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit7/AfterSymfonyListener.php
new file mode 100644
index 000000000000..a7f15e5b0fa0
--- /dev/null
+++ b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit7/AfterSymfonyListener.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\TestTools\PhpUnitCompatibility\PhpUnit7;
+
+use PHPUnit\Framework\Test;
+use PHPUnit\Framework\TestListener;
+use PHPUnit\Framework\TestListenerDefaultImplementation;
+
+/**
+ * Listens to PHPUnit test runs.
+ *
+ * @internal
+ */
+class AfterSymfonyListener implements TestListener {
+  use TestListenerDefaultImplementation;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function endTest(Test $test, float $time): void {
+    restore_error_handler();
+  }
+
+}
diff --git a/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit7/DrupalListener.php b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit7/DrupalListener.php
new file mode 100644
index 000000000000..e189bff93743
--- /dev/null
+++ b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit7/DrupalListener.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Drupal\TestTools\PhpUnitCompatibility\PhpUnit7;
+
+use Drupal\Tests\Listeners\DeprecationListenerTrait;
+use Drupal\Tests\Listeners\DrupalComponentTestListenerTrait;
+use Drupal\Tests\Listeners\DrupalStandardsListenerTrait;
+use PHPUnit\Framework\TestListener;
+use PHPUnit\Framework\TestListenerDefaultImplementation;
+use PHPUnit\Framework\Test;
+
+/**
+ * Listens to PHPUnit test runs.
+ *
+ * @internal
+ */
+class DrupalListener implements TestListener {
+
+  use TestListenerDefaultImplementation;
+  use DeprecationListenerTrait;
+  use DrupalComponentTestListenerTrait;
+  use DrupalStandardsListenerTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function startTest(Test $test): void {
+    $this->deprecationStartTest($test);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function endTest(Test $test, float $time): void {
+    $this->deprecationEndTest($test, $time);
+    $this->componentEndTest($test, $time);
+    $this->standardsEndTest($test, $time);
+  }
+
+}
diff --git a/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit7/FileFieldTestBaseTrait.php b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit7/FileFieldTestBaseTrait.php
new file mode 100644
index 000000000000..7d055db63fe6
--- /dev/null
+++ b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit7/FileFieldTestBaseTrait.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace Drupal\TestTools\PhpUnitCompatibility\PhpUnit7;
+
+/**
+ * Makes Drupal's test API forward compatible with multiple versions of PHPUnit.
+ */
+trait FileFieldTestBaseTrait {
+
+  // @todo remove in Drupal 9.
+
+}
diff --git a/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit7/HtmlOutputPrinter.php b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit7/HtmlOutputPrinter.php
new file mode 100644
index 000000000000..64226fa96db0
--- /dev/null
+++ b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit7/HtmlOutputPrinter.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\TestTools\PhpUnitCompatibility\PhpUnit7;
+
+use Drupal\Tests\Listeners\HtmlOutputPrinterTrait;
+use PHPUnit\Framework\TestResult;
+use PHPUnit\TextUI\ResultPrinter;
+
+/**
+ * Defines a class for providing html output results for functional tests.
+ *
+ * @internal
+ */
+class HtmlOutputPrinter extends ResultPrinter {
+
+  use HtmlOutputPrinterTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function printResult(TestResult $result): void {
+    parent::printResult($result);
+
+    $this->printHtmlOutput();
+  }
+
+}
diff --git a/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit7/SimpletestUiPrinter.php b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit7/SimpletestUiPrinter.php
new file mode 100644
index 000000000000..0df6f003e1a9
--- /dev/null
+++ b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit7/SimpletestUiPrinter.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Drupal\TestTools\PhpUnitCompatibility\PhpUnit7;
+
+/**
+ * Defines a class for providing html output links in the Simpletest UI.
+ */
+class SimpletestUiPrinter extends HtmlOutputPrinter {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function write(string $buffer): void {
+    $this->simpletestUiWrite($buffer);
+  }
+
+}
diff --git a/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit7/StubTestSuiteBaseTrait.php b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit7/StubTestSuiteBaseTrait.php
new file mode 100644
index 000000000000..6c00ed955c13
--- /dev/null
+++ b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit7/StubTestSuiteBaseTrait.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Drupal\TestTools\PhpUnitCompatibility\PhpUnit7;
+
+/**
+ * Makes Drupal's test API forward compatible with multiple versions of PHPUnit.
+ */
+trait StubTestSuiteBaseTrait {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addTestFiles($filenames): void {
+    // We stub addTestFiles() because the parent implementation can't deal with
+    // vfsStream-based filesystems due to an error in
+    // stream_resolve_include_path(). See
+    // https://github.com/mikey179/vfsStream/issues/5 Here we just store the
+    // test file being added in $this->testFiles.
+    $this->testFiles = array_merge($this->testFiles, $filenames);
+  }
+
+}
diff --git a/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit7/TestCompatibilityTrait.php b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit7/TestCompatibilityTrait.php
new file mode 100644
index 000000000000..a80dfbbf7909
--- /dev/null
+++ b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit7/TestCompatibilityTrait.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\TestTools\PhpUnitCompatibility\PhpUnit7;
+
+/**
+ * Makes Drupal's test API forward compatible with multiple versions of PHPUnit.
+ */
+trait TestCompatibilityTrait {
+
+  /**
+   * @todo deprecate this method override in
+   *   https://www.drupal.org/project/drupal/issues/2742585
+   *
+   * @see \Drupal\simpletest\TestBase::assertTrue()
+   */
+  public static function assertTrue($actual, string $message = ''): void {
+    if (is_bool($actual)) {
+      parent::assertTrue($actual, $message);
+    }
+    else {
+      parent::assertNotEmpty($actual, $message);
+    }
+  }
+
+  /**
+   * @todo deprecate this method override in
+   *   https://www.drupal.org/project/drupal/issues/2742585
+   *
+   * @see \Drupal\simpletest\TestBase::assertFalse()
+   */
+  public static function assertFalse($actual, string $message = ''): void {
+    if (is_bool($actual)) {
+      parent::assertFalse($actual, $message);
+    }
+    else {
+      parent::assertEmpty($actual, $message);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function assertEquals($expected, $actual, string $message = '', float $delta = 0, int $maxDepth = 10, bool $canonicalize = FALSE, bool $ignoreCase = FALSE): void {
+    // Cast objects implementing MarkupInterface to string instead of
+    // relying on PHP casting them to string depending on what they are being
+    // comparing with.
+    if (method_exists(self::class, 'castSafeStrings')) {
+      $expected = self::castSafeStrings($expected);
+      $actual = self::castSafeStrings($actual);
+    }
+    parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
+  }
+
+}
diff --git a/core/tests/Drupal/TestTools/PhpUnitCompatibility/RunnerVersion.php b/core/tests/Drupal/TestTools/PhpUnitCompatibility/RunnerVersion.php
new file mode 100644
index 000000000000..b929b02d30e7
--- /dev/null
+++ b/core/tests/Drupal/TestTools/PhpUnitCompatibility/RunnerVersion.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Drupal\TestTools\PhpUnitCompatibility;
+
+use PHPUnit\Runner\Version;
+
+/**
+ * Helper class to determine information about running PHPUnit version.
+ *
+ * This class contains static methods only and is not meant to be instantiated.
+ */
+final class RunnerVersion {
+
+  /**
+   * This class should not be instantiated.
+   */
+  private function __construct() {
+  }
+
+  /**
+   * Returns the major version of the PHPUnit runner being used.
+   *
+   * @return int
+   *   The major version of the PHPUnit runner being used.
+   */
+  public static function getMajor() {
+    return (int) explode('.', Version::id())[0];
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/BrowserTestBase.php b/core/tests/Drupal/Tests/BrowserTestBase.php
index 759b6033eabb..96657a93e793 100644
--- a/core/tests/Drupal/Tests/BrowserTestBase.php
+++ b/core/tests/Drupal/Tests/BrowserTestBase.php
@@ -688,18 +688,6 @@ protected function getDrupalSettings() {
     return [];
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public static function assertEquals($expected, $actual, $message = '', $delta = 0.0, $maxDepth = 10, $canonicalize = FALSE, $ignoreCase = FALSE) {
-    // Cast objects implementing MarkupInterface to string instead of
-    // relying on PHP casting them to string depending on what they are being
-    // comparing with.
-    $expected = static::castSafeStrings($expected);
-    $actual = static::castSafeStrings($actual);
-    parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
-  }
-
   /**
    * Retrieves the current calling line in the class under test.
    *
@@ -710,9 +698,14 @@ protected function getTestMethodCaller() {
     $backtrace = debug_backtrace();
     // Find the test class that has the test method.
     while ($caller = Error::getLastCaller($backtrace)) {
-      if (isset($caller['class']) && $caller['class'] === get_class($this)) {
+      // If we match PHPUnit's TestCase::runTest, then the previously processed
+      // caller entry is where our test method sits.
+      if (isset($last_caller) && isset($caller['function']) && $caller['function'] === 'PHPUnit\Framework\TestCase->runTest()') {
+        // Return the last caller since that has to be the test class.
+        $caller = $last_caller;
         break;
       }
+
       // If the test method is implemented by a test class's parent then the
       // class name of $this will not be part of the backtrace.
       // In that case we process the backtrace until the caller is not a
@@ -722,6 +715,11 @@ protected function getTestMethodCaller() {
         $caller = $last_caller;
         break;
       }
+
+      if (isset($caller['class']) && $caller['class'] === get_class($this)) {
+        break;
+      }
+
       // Otherwise we have not reached our test class yet: save the last caller
       // and remove an element from to backtrace to process the next call.
       $last_caller = $caller;
diff --git a/core/tests/Drupal/Tests/Core/Test/TestSuiteBaseTest.php b/core/tests/Drupal/Tests/Core/Test/TestSuiteBaseTest.php
index 853a788f5fee..d593cd29b820 100644
--- a/core/tests/Drupal/Tests/Core/Test/TestSuiteBaseTest.php
+++ b/core/tests/Drupal/Tests/Core/Test/TestSuiteBaseTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\Core\Test;
 
+use Drupal\TestTools\PhpUnitCompatibility\RunnerVersion;
 use Drupal\Tests\TestSuites\TestSuiteBase;
 use org\bovigo\vfs\vfsStream;
 use PHPUnit\Framework\TestCase;
@@ -10,6 +11,13 @@
 // manually.
 require_once __DIR__ . '/../../../../TestSuites/TestSuiteBase.php';
 
+// In order to manage different method signatures between PHPUnit versions, we
+// dynamically load a compatibility trait dependent on the PHPUnit runner
+// version.
+if (!trait_exists(PhpunitVersionDependentStubTestSuiteBaseTrait::class, FALSE)) {
+  class_alias("Drupal\TestTools\PhpUnitCompatibility\PhpUnit" . RunnerVersion::getMajor() . "\StubTestSuiteBaseTrait", PhpunitVersionDependentStubTestSuiteBaseTrait::class);
+}
+
 /**
  * @coversDefaultClass \Drupal\Tests\TestSuites\TestSuiteBase
  *
@@ -120,6 +128,8 @@ public function testLocalTimeZone() {
  */
 class StubTestSuiteBase extends TestSuiteBase {
 
+  use PhpunitVersionDependentStubTestSuiteBaseTrait;
+
   /**
    * Test files discovered by addTestsBySuiteNamespace().
    *
@@ -139,16 +149,4 @@ protected function findExtensionDirectories($root) {
     return [];
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function addTestFiles($filenames) {
-    // We stub addTestFiles() because the parent implementation can't deal with
-    // vfsStream-based filesystems due to an error in
-    // stream_resolve_include_path(). See
-    // https://github.com/mikey179/vfsStream/issues/5 Here we just store the
-    // test file being added in $this->testFiles.
-    $this->testFiles = array_merge($this->testFiles, $filenames);
-  }
-
 }
diff --git a/core/tests/Drupal/Tests/Listeners/AfterSymfonyListener.php b/core/tests/Drupal/Tests/Listeners/AfterSymfonyListener.php
index 8806decd1cd0..d9cb992f239a 100644
--- a/core/tests/Drupal/Tests/Listeners/AfterSymfonyListener.php
+++ b/core/tests/Drupal/Tests/Listeners/AfterSymfonyListener.php
@@ -1,24 +1,15 @@
 <?php
 
-namespace Drupal\Tests\Listeners;
-
-use PHPUnit\Framework\Test;
-use PHPUnit\Framework\TestListener;
-use PHPUnit\Framework\TestListenerDefaultImplementation;
-
 /**
+ * @file
  * Listens to PHPUnit test runs.
  *
- * @internal
+ * In order to manage different method signatures between PHPUnit versions, we
+ * dynamically load a class dependent on the PHPUnit runner version.
  */
-class AfterSymfonyListener implements TestListener {
-  use TestListenerDefaultImplementation;
 
-  /**
-   * {@inheritdoc}
-   */
-  public function endTest(Test $test, $time) {
-    restore_error_handler();
-  }
+namespace Drupal\Tests\Listeners;
+
+use Drupal\TestTools\PhpUnitCompatibility\RunnerVersion;
 
-}
+class_alias("Drupal\TestTools\PhpUnitCompatibility\PhpUnit" . RunnerVersion::getMajor() . "\AfterSymfonyListener", AfterSymfonyListener::class);
diff --git a/core/tests/Drupal/Tests/Listeners/DrupalListener.php b/core/tests/Drupal/Tests/Listeners/DrupalListener.php
index 8b7966a26d65..ba5afbb57b12 100644
--- a/core/tests/Drupal/Tests/Listeners/DrupalListener.php
+++ b/core/tests/Drupal/Tests/Listeners/DrupalListener.php
@@ -1,36 +1,15 @@
 <?php
 
-namespace Drupal\Tests\Listeners;
-
-use PHPUnit\Framework\Test;
-use PHPUnit\Framework\TestListener;
-use PHPUnit\Framework\TestListenerDefaultImplementation;
-
 /**
+ * @file
  * Listens to PHPUnit test runs.
  *
- * @internal
+ * In order to manage different method signatures between PHPUnit versions, we
+ * dynamically load a class dependent on the PHPUnit runner version.
  */
-class DrupalListener implements TestListener {
-  use TestListenerDefaultImplementation;
-  use DeprecationListenerTrait;
-  use DrupalComponentTestListenerTrait;
-  use DrupalStandardsListenerTrait;
 
-  /**
-   * {@inheritdoc}
-   */
-  public function startTest(Test $test) {
-    $this->deprecationStartTest($test);
-  }
+namespace Drupal\Tests\Listeners;
 
-  /**
-   * {@inheritdoc}
-   */
-  public function endTest(Test $test, $time) {
-    $this->deprecationEndTest($test, $time);
-    $this->componentEndTest($test, $time);
-    $this->standardsEndTest($test, $time);
-  }
+use Drupal\TestTools\PhpUnitCompatibility\RunnerVersion;
 
-}
+class_alias("Drupal\TestTools\PhpUnitCompatibility\PhpUnit" . RunnerVersion::getMajor() . "\DrupalListener", DrupalListener::class);
diff --git a/core/tests/Drupal/Tests/Listeners/HtmlOutputPrinter.php b/core/tests/Drupal/Tests/Listeners/HtmlOutputPrinter.php
index b29b11d19df3..908c48a5ea55 100644
--- a/core/tests/Drupal/Tests/Listeners/HtmlOutputPrinter.php
+++ b/core/tests/Drupal/Tests/Listeners/HtmlOutputPrinter.php
@@ -1,34 +1,15 @@
 <?php
 
-namespace Drupal\Tests\Listeners;
-
-use PHPUnit\Framework\TestResult;
-use PHPUnit\TextUI\ResultPrinter;
-
 /**
+ * @file
  * Defines a class for providing html output results for functional tests.
  *
- * @internal
+ * In order to manage different method signatures between PHPUnit versions, we
+ * dynamically load a class dependent on the PHPUnit runner version.
  */
-class HtmlOutputPrinter extends ResultPrinter {
-  use HtmlOutputPrinterTrait;
 
-  /**
-   * {@inheritdoc}
-   */
-  public function __construct($out = NULL, $verbose = FALSE, $colors = self::COLOR_DEFAULT, $debug = FALSE, $numberOfColumns = 80, $reverse = FALSE) {
-    parent::__construct($out, $verbose, $colors, $debug, $numberOfColumns, $reverse);
-
-    $this->setUpHtmlOutput();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function printResult(TestResult $result) {
-    parent::printResult($result);
+namespace Drupal\Tests\Listeners;
 
-    $this->printHtmlOutput();
-  }
+use Drupal\TestTools\PhpUnitCompatibility\RunnerVersion;
 
-}
+class_alias("Drupal\TestTools\PhpUnitCompatibility\PhpUnit" . RunnerVersion::getMajor() . "\HtmlOutputPrinter", HtmlOutputPrinter::class);
diff --git a/core/tests/Drupal/Tests/Listeners/HtmlOutputPrinterTrait.php b/core/tests/Drupal/Tests/Listeners/HtmlOutputPrinterTrait.php
index 1dd67eb9e9a3..6cdd1e80029c 100644
--- a/core/tests/Drupal/Tests/Listeners/HtmlOutputPrinterTrait.php
+++ b/core/tests/Drupal/Tests/Listeners/HtmlOutputPrinterTrait.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\Tests\Listeners;
 
+use Drupal\Component\Utility\Html;
+
 /**
  * Defines a class for providing html output results for functional tests.
  *
@@ -16,6 +18,15 @@ trait HtmlOutputPrinterTrait {
    */
   protected $browserOutputFile;
 
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct($out = NULL, $verbose = FALSE, $colors = self::COLOR_DEFAULT, $debug = FALSE, $numberOfColumns = 80, $reverse = FALSE) {
+    parent::__construct($out, $verbose, $colors, $debug, $numberOfColumns, $reverse);
+
+    $this->setUpHtmlOutput();
+  }
+
   /**
    * Creates the file to list the HTML output created during the test.
    *
@@ -69,4 +80,18 @@ protected function printHtmlOutput() {
     }
   }
 
+  /**
+   * Prints HTML output links for the Simpletest UI.
+   */
+  public function simpletestUiWrite($buffer) {
+    $buffer = Html::escape($buffer);
+    // Turn HTML output URLs into clickable link <a> tags.
+    $url_pattern = '@https?://[^\s]+@';
+    $buffer = preg_replace($url_pattern, '<a href="$0" target="_blank" title="$0">$0</a>', $buffer);
+    // Make the output readable in HTML by breaking up lines properly.
+    $buffer = nl2br($buffer);
+
+    print $buffer;
+  }
+
 }
diff --git a/core/tests/Drupal/Tests/Listeners/SimpletestUiPrinter.php b/core/tests/Drupal/Tests/Listeners/SimpletestUiPrinter.php
index bfb91d7b9fc0..18151dd90661 100644
--- a/core/tests/Drupal/Tests/Listeners/SimpletestUiPrinter.php
+++ b/core/tests/Drupal/Tests/Listeners/SimpletestUiPrinter.php
@@ -1,26 +1,15 @@
 <?php
 
-namespace Drupal\Tests\Listeners;
-
-use Drupal\Component\Utility\Html;
-
 /**
+ * @file
  * Defines a class for providing html output links in the Simpletest UI.
+ *
+ * In order to manage different method signatures between PHPUnit versions, we
+ * dynamically load a class dependent on the PHPUnit runner version.
  */
-class SimpletestUiPrinter extends HtmlOutputPrinter {
 
-  /**
-   * {@inheritdoc}
-   */
-  public function write($buffer) {
-    $buffer = Html::escape($buffer);
-    // Turn HTML output URLs into clickable link <a> tags.
-    $url_pattern = '@https?://[^\s]+@';
-    $buffer = preg_replace($url_pattern, '<a href="$0" target="_blank" title="$0">$0</a>', $buffer);
-    // Make the output readable in HTML by breaking up lines properly.
-    $buffer = nl2br($buffer);
+namespace Drupal\Tests\Listeners;
 
-    print $buffer;
-  }
+use Drupal\TestTools\PhpUnitCompatibility\RunnerVersion;
 
-}
+class_alias("Drupal\TestTools\PhpUnitCompatibility\PhpUnit" . RunnerVersion::getMajor() . "\SimpletestUiPrinter", SimpletestUiPrinter::class);
diff --git a/core/tests/Drupal/Tests/PhpunitCompatibilityTrait.php b/core/tests/Drupal/Tests/PhpunitCompatibilityTrait.php
index f6460764b4d6..8b52d8707e15 100644
--- a/core/tests/Drupal/Tests/PhpunitCompatibilityTrait.php
+++ b/core/tests/Drupal/Tests/PhpunitCompatibilityTrait.php
@@ -2,11 +2,22 @@
 
 namespace Drupal\Tests;
 
+use Drupal\TestTools\PhpUnitCompatibility\RunnerVersion;
+
+// In order to manage different method signatures between PHPUnit versions, we
+// dynamically load a compatibility trait dependent on the PHPUnit runner
+// version.
+if (!trait_exists(PhpunitVersionDependentTestCompatibilityTrait::class, FALSE)) {
+  class_alias("Drupal\TestTools\PhpUnitCompatibility\PhpUnit" . RunnerVersion::getMajor() . "\TestCompatibilityTrait", PhpunitVersionDependentTestCompatibilityTrait::class);
+}
+
 /**
  * Makes Drupal's test API forward compatible with multiple versions of PHPUnit.
  */
 trait PhpunitCompatibilityTrait {
 
+  use PhpunitVersionDependentTestCompatibilityTrait;
+
   /**
    * Returns a mock object for the specified class using the available method.
    *
diff --git a/core/tests/Drupal/Tests/TestRequirementsTrait.php b/core/tests/Drupal/Tests/TestRequirementsTrait.php
index 2c320dfaaf61..f43c7a222aca 100644
--- a/core/tests/Drupal/Tests/TestRequirementsTrait.php
+++ b/core/tests/Drupal/Tests/TestRequirementsTrait.php
@@ -3,6 +3,7 @@
 namespace Drupal\Tests;
 
 use Drupal\Core\Extension\ExtensionDiscovery;
+use PHPUnit\Util\Test;
 use PHPUnit\Framework\SkippedTestError;
 
 /**
@@ -34,7 +35,18 @@ protected static function getDrupalRoot() {
    *   skipped. Callers should not catch this exception.
    */
   protected function checkRequirements() {
-    parent::checkRequirements();
+    if (!$this->getName(FALSE) || !method_exists($this, $this->getName(FALSE))) {
+      return;
+    }
+
+    $missingRequirements = Test::getMissingRequirements(
+      get_class($this),
+      $this->getName(FALSE)
+    );
+
+    if (!empty($missingRequirements)) {
+      $this->markTestSkipped(implode(PHP_EOL, $missingRequirements));
+    }
 
     $root = static::getDrupalRoot();
 
diff --git a/core/tests/Drupal/Tests/UnitTestCase.php b/core/tests/Drupal/Tests/UnitTestCase.php
index d8cfbec16309..4a2c012260be 100644
--- a/core/tests/Drupal/Tests/UnitTestCase.php
+++ b/core/tests/Drupal/Tests/UnitTestCase.php
@@ -90,7 +90,7 @@ protected function getRandomGenerator() {
   protected function assertArrayEquals(array $expected, array $actual, $message = NULL) {
     ksort($expected);
     ksort($actual);
-    $this->assertEquals($expected, $actual, $message);
+    $this->assertEquals($expected, $actual, !empty($message) ? $message : '');
   }
 
   /**
diff --git a/core/tests/bootstrap.php b/core/tests/bootstrap.php
index 6d30a8b7ea87..467e6af6e603 100644
--- a/core/tests/bootstrap.php
+++ b/core/tests/bootstrap.php
@@ -143,6 +143,7 @@ function drupal_phpunit_populate_class_loader() {
   $loader->add('Drupal\\KernelTests', __DIR__);
   $loader->add('Drupal\\FunctionalTests', __DIR__);
   $loader->add('Drupal\\FunctionalJavascriptTests', __DIR__);
+  $loader->add('Drupal\\TestTools', __DIR__);
 
   if (!isset($GLOBALS['namespaces'])) {
     // Scan for arbitrary extension namespaces from core and contrib.
-- 
GitLab