diff --git a/composer.json b/composer.json
index 0b52046f036cf8019ae281578600dec794db09e3..1bd76c125e4bc9f138982da3fe82e2fe831ae7ac 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 55d81156b7d60c3600b0b116154016dae11a31d2..0b54bbfb9a544e8f95fda61d720edfd2368daf75 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 ed0303312fa81ee82e00daa19259d9171721897a..627972c21c991f0d2d04ff9d854e2e7cda983e24 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 552a72acf9e9fae9d0e852d34603645bed8475ad..7c6242f950aeb8aa90416ec972b7dd50ef5ae43a 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 ba47d91307ce0b6be3a67cc2455e814fcbcfb844..f9a82a4d546ba012f0521249ca277c35f11609a1 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 d738c9ab72a54ed9e92f5f47f21db51f209868b0..fc57f1747fdf304e98f070a52c0a7ba577bdaf2f 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 ec7b99d63df9d45f6e626c5799288509f101b38b..5c7b5cfff1a3f6f3a45a8196ef1b98ed6e4b5a7e 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 98051262ec288ecaf838ee603ce06b4d95342dce..7e9dae071b8e85e0144bae8853bc788e350f0dda 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 36fd9b0b42a3c3e35593558f5127ae6ca1a66f41..6915e4dbe4f4b1dad8b40044116a02ca65e3c0e8 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 39ae5ff69f2c656cafe82d2790edfbb49f61244d..528b39f9c612081ee3e5aea1fc275e12a3e148fe 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 8a0fd574b2cfc8df349d5441e2ef6097a462df71..1360798e46187b739d583d5bca819dc4dcb68a90 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 8d51d254375a1ecbcbf2b20b39b3ef40d5de4396..058669b590a93fa598ea3e166e4a9da063c409f1 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 ad46657cf646d1f35100c8d18a4c44b9c6fc2f85..dfda4a9fc060f47ddc139e96227e0603bdc249ce 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 0000000000000000000000000000000000000000..92732557f18e6ae6679614dd5c1c9a6a68246e01
--- /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 0000000000000000000000000000000000000000..4cb66a1797eee4cebf14e6a09d13a2cf0d1041c8
--- /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 0000000000000000000000000000000000000000..212485f90ca2e21249dacfaa5f28b64ccebdc093
--- /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 0000000000000000000000000000000000000000..c3a6a1821f1b9a0ce8912974835c68f7931db8ed
--- /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 0000000000000000000000000000000000000000..5ea80f48e4c5da7ff393b08d2653c00c910a6a93
--- /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 0000000000000000000000000000000000000000..80ae1901c2ab8912a4cb7a30c7a3bb9f00fc951a
--- /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 0000000000000000000000000000000000000000..0785472243c3cc2fc5c8ebacc617e86fe08941a3
--- /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 0000000000000000000000000000000000000000..a7f15e5b0fa0986be5b22b6dfa03074d07317fba
--- /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 0000000000000000000000000000000000000000..e189bff93743c17c67eb839324eaf1fb1b5887a0
--- /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 0000000000000000000000000000000000000000..7d055db63fe6ab19e640cbf73b8b3f21df227d14
--- /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 0000000000000000000000000000000000000000..64226fa96db04fbbdcbe3d97f711447fc7666d57
--- /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 0000000000000000000000000000000000000000..0df6f003e1a9e3d669acbae0b5208aa2de2257e7
--- /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 0000000000000000000000000000000000000000..6c00ed955c1361eddf4d135b7ac4a9312d75326a
--- /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 0000000000000000000000000000000000000000..a80dfbbf79090b2e95fcb80daf9fe08acabf9d19
--- /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 0000000000000000000000000000000000000000..b929b02d30e71f9ae97a2954bf1ba1f61c10f57f
--- /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 759b6033eabb22a3a1ad6ba504844a570bde67c5..96657a93e7932d59ec5bb79a0738365cccee35a4 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 853a788f5feec316008868f78995556938b0c98d..d593cd29b820e39c6d8d7e0f09a5e0c5c4c2c89e 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 8806decd1cd0ae7b081a98c2e92ab95d4687ad68..d9cb992f239a920573abe9cbd8ae2b5f83c14e7c 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 8b7966a26d658bb74a0ba4af53fd0c021cce8d3c..ba5afbb57b125de7790d90e93012a9b864df4d27 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 b29b11d19df3ae9d5902807e9d48d54f7c0b59a6..908c48a5ea550b26b19911438b06995894803516 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 1dd67eb9e9a3e5367594d3049c3865ad2d1e3353..6cdd1e80029c1c60b8ecdb1c985c21296fe90dab 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 bfb91d7b9fc01580308e7716894b2f8622d8eb0e..18151dd906613fdc2f4d28b06aebf440f4e81dee 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 f6460764b4d63de01f0fe236a6e854697fdc70eb..8b52d8707e155790ea42bec4b559099232b447ba 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 2c320dfaaf61017094d7543bf41edec12aa829ea..f43c7a222aca64b12071dd81c545dded1ef0aa62 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 d8cfbec16309122cd062fabdbe2f634ca86c92f4..4a2c012260be2e983f13eb94c3d5c9a49081ad9a 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 6d30a8b7ea8735052b9be97e6e0befdb0c291173..467e6af6e603900cbcfe87e3ed54c19c63b1855a 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.