diff --git a/composer.json b/composer.json
index 2a4ba65b33..5721b5c8c6 100644
--- a/composer.json
+++ b/composer.json
@@ -17,7 +17,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",
@@ -56,7 +56,8 @@
     },
     "autoload": {
         "psr-4": {
-            "Drupal\\Core\\Composer\\": "core/lib/Drupal/Core/Composer"
+            "Drupal\\Core\\Composer\\": "core/lib/Drupal/Core/Composer",
+            "Drupal\\Tool\\": "core/tools"
         }
     },
     "autoload-dev": {
diff --git a/core/drupalci.yml b/core/drupalci.yml
index 6ce07ee7ad..070da19642 100644
--- a/core/drupalci.yml
+++ b/core/drupalci.yml
@@ -15,6 +15,10 @@ build:
         sniff-all-files: false
         halt-on-fail: false
     testing:
+      # Update PHPUnit & friends.
+      container_command:
+        commands:
+          - "sudo -u www-data /usr/local/bin/composer update phpunit/phpunit symfony/phpunit-bridge phpspec/prophecy symfony/yaml --with-dependencies --no-progress"
       # run_tests task is executed several times in order of performance speeds.
       # halt-on-fail can be set on the run_tests tasks in order to fail fast.
       # suppress-deprecations is false in order to be alerted to usages of
diff --git a/core/modules/field/tests/src/Kernel/Number/NumberItemTest.php b/core/modules/field/tests/src/Kernel/Number/NumberItemTest.php
index ba47d91307..f9a82a4d54 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 d738c9ab72..a3552f8685 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\Tool\PhpUnit\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\Tool\PhpUnit\PhpUnitCompatibility\PhpUnit" . RunnerVersion::getMajor() . "\FileFieldTestBaseTrait", PhpunitVersionDependentFileFieldTestBaseTrait::class);
+}
+
 /**
  * Provides methods specifically for testing File module's field handling.
  */
@@ -19,6 +27,7 @@
   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 ec7b99d63d..6cae2c16a1 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\Tool\PhpUnit\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 98051262ec..7e9dae071b 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 36fd9b0b42..6915e4dbe4 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/tests/Drupal/KernelTests/AssertLegacyTrait.php b/core/tests/Drupal/KernelTests/AssertLegacyTrait.php
index 8a0fd574b2..17a2ea4133 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, !empty($message) ? $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, !empty($message) ? $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, !empty($message) ? $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, !empty($message) ? $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, !empty($message) ? $message : '');
   }
 
   /**
diff --git a/core/tests/Drupal/KernelTests/Core/Theme/ThemeRenderAndAutoescapeTest.php b/core/tests/Drupal/KernelTests/Core/Theme/ThemeRenderAndAutoescapeTest.php
index 8d51d25437..058669b590 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 fd26d08a14..e26f5e0e15 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/Tests/BrowserTestBase.php b/core/tests/Drupal/Tests/BrowserTestBase.php
index 759b6033ea..96657a93e7 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 853a788f5f..946a47b162 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\Tool\PhpUnit\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\Tool\PhpUnit\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 8806decd1c..a6ad26baf3 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\Tool\PhpUnit\PhpUnitCompatibility\RunnerVersion;
 
-}
+class_alias("Drupal\Tool\PhpUnit\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 8b7966a26d..5fd3d34742 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\Tool\PhpUnit\PhpUnitCompatibility\RunnerVersion;
 
-}
+class_alias("Drupal\Tool\PhpUnit\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 b29b11d19d..c53ad8eb3b 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\Tool\PhpUnit\PhpUnitCompatibility\RunnerVersion;
 
-}
+class_alias("Drupal\Tool\PhpUnit\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 1dd67eb9e9..6cdd1e8002 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 @@
    */
   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 bfb91d7b9f..c34edca957 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\Tool\PhpUnit\PhpUnitCompatibility\RunnerVersion;
 
-}
+class_alias("Drupal\Tool\PhpUnit\PhpUnitCompatibility\PhpUnit" . RunnerVersion::getMajor() . "\SimpletestUiPrinter", SimpletestUiPrinter::class);
diff --git a/core/tests/Drupal/Tests/PhpunitCompatibilityTrait.php b/core/tests/Drupal/Tests/PhpunitCompatibilityTrait.php
index f6460764b4..8f604bc68b 100644
--- a/core/tests/Drupal/Tests/PhpunitCompatibilityTrait.php
+++ b/core/tests/Drupal/Tests/PhpunitCompatibilityTrait.php
@@ -2,11 +2,22 @@
 
 namespace Drupal\Tests;
 
+use Drupal\Tool\PhpUnit\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\Tool\PhpUnit\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 2c320dfaaf..f43c7a222a 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 d8cfbec163..4a2c012260 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/tools/PhpUnit/PhpUnitCompatibility/PhpUnit6/AfterSymfonyListener.php b/core/tools/PhpUnit/PhpUnitCompatibility/PhpUnit6/AfterSymfonyListener.php
new file mode 100644
index 0000000000..a1c0ec4bf0
--- /dev/null
+++ b/core/tools/PhpUnit/PhpUnitCompatibility/PhpUnit6/AfterSymfonyListener.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Tool\PhpUnit\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/tools/PhpUnit/PhpUnitCompatibility/PhpUnit6/DrupalListener.php b/core/tools/PhpUnit/PhpUnitCompatibility/PhpUnit6/DrupalListener.php
new file mode 100644
index 0000000000..c2dad8b61d
--- /dev/null
+++ b/core/tools/PhpUnit/PhpUnitCompatibility/PhpUnit6/DrupalListener.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Drupal\Tool\PhpUnit\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/tools/PhpUnit/PhpUnitCompatibility/PhpUnit6/FileFieldTestBaseTrait.php b/core/tools/PhpUnit/PhpUnitCompatibility/PhpUnit6/FileFieldTestBaseTrait.php
new file mode 100644
index 0000000000..f832edcaba
--- /dev/null
+++ b/core/tools/PhpUnit/PhpUnitCompatibility/PhpUnit6/FileFieldTestBaseTrait.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Drupal\Tool\PhpUnit\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/tools/PhpUnit/PhpUnitCompatibility/PhpUnit6/HtmlOutputPrinter.php b/core/tools/PhpUnit/PhpUnitCompatibility/PhpUnit6/HtmlOutputPrinter.php
new file mode 100644
index 0000000000..3008313161
--- /dev/null
+++ b/core/tools/PhpUnit/PhpUnitCompatibility/PhpUnit6/HtmlOutputPrinter.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\Tool\PhpUnit\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/tools/PhpUnit/PhpUnitCompatibility/PhpUnit6/SimpletestUiPrinter.php b/core/tools/PhpUnit/PhpUnitCompatibility/PhpUnit6/SimpletestUiPrinter.php
new file mode 100644
index 0000000000..c15ee3af2e
--- /dev/null
+++ b/core/tools/PhpUnit/PhpUnitCompatibility/PhpUnit6/SimpletestUiPrinter.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Drupal\Tool\PhpUnit\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/tools/PhpUnit/PhpUnitCompatibility/PhpUnit6/StubTestSuiteBaseTrait.php b/core/tools/PhpUnit/PhpUnitCompatibility/PhpUnit6/StubTestSuiteBaseTrait.php
new file mode 100644
index 0000000000..b758307424
--- /dev/null
+++ b/core/tools/PhpUnit/PhpUnitCompatibility/PhpUnit6/StubTestSuiteBaseTrait.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Drupal\Tool\PhpUnit\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/tools/PhpUnit/PhpUnitCompatibility/PhpUnit6/TestCompatibilityTrait.php b/core/tools/PhpUnit/PhpUnitCompatibility/PhpUnit6/TestCompatibilityTrait.php
new file mode 100644
index 0000000000..d36d06a597
--- /dev/null
+++ b/core/tools/PhpUnit/PhpUnitCompatibility/PhpUnit6/TestCompatibilityTrait.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\Tool\PhpUnit\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/tools/PhpUnit/PhpUnitCompatibility/PhpUnit7/AfterSymfonyListener.php b/core/tools/PhpUnit/PhpUnitCompatibility/PhpUnit7/AfterSymfonyListener.php
new file mode 100644
index 0000000000..7353b705c2
--- /dev/null
+++ b/core/tools/PhpUnit/PhpUnitCompatibility/PhpUnit7/AfterSymfonyListener.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\Tool\PhpUnit\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/tools/PhpUnit/PhpUnitCompatibility/PhpUnit7/DrupalListener.php b/core/tools/PhpUnit/PhpUnitCompatibility/PhpUnit7/DrupalListener.php
new file mode 100644
index 0000000000..41ad00c443
--- /dev/null
+++ b/core/tools/PhpUnit/PhpUnitCompatibility/PhpUnit7/DrupalListener.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Drupal\Tool\PhpUnit\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/tools/PhpUnit/PhpUnitCompatibility/PhpUnit7/FileFieldTestBaseTrait.php b/core/tools/PhpUnit/PhpUnitCompatibility/PhpUnit7/FileFieldTestBaseTrait.php
new file mode 100644
index 0000000000..a36bbb80b1
--- /dev/null
+++ b/core/tools/PhpUnit/PhpUnitCompatibility/PhpUnit7/FileFieldTestBaseTrait.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace Drupal\Tool\PhpUnit\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/tools/PhpUnit/PhpUnitCompatibility/PhpUnit7/HtmlOutputPrinter.php b/core/tools/PhpUnit/PhpUnitCompatibility/PhpUnit7/HtmlOutputPrinter.php
new file mode 100644
index 0000000000..d481feeaff
--- /dev/null
+++ b/core/tools/PhpUnit/PhpUnitCompatibility/PhpUnit7/HtmlOutputPrinter.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\Tool\PhpUnit\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/tools/PhpUnit/PhpUnitCompatibility/PhpUnit7/SimpletestUiPrinter.php b/core/tools/PhpUnit/PhpUnitCompatibility/PhpUnit7/SimpletestUiPrinter.php
new file mode 100644
index 0000000000..3759bd6394
--- /dev/null
+++ b/core/tools/PhpUnit/PhpUnitCompatibility/PhpUnit7/SimpletestUiPrinter.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Drupal\Tool\PhpUnit\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/tools/PhpUnit/PhpUnitCompatibility/PhpUnit7/StubTestSuiteBaseTrait.php b/core/tools/PhpUnit/PhpUnitCompatibility/PhpUnit7/StubTestSuiteBaseTrait.php
new file mode 100644
index 0000000000..b41e57c20c
--- /dev/null
+++ b/core/tools/PhpUnit/PhpUnitCompatibility/PhpUnit7/StubTestSuiteBaseTrait.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Drupal\Tool\PhpUnit\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/tools/PhpUnit/PhpUnitCompatibility/PhpUnit7/TestCompatibilityTrait.php b/core/tools/PhpUnit/PhpUnitCompatibility/PhpUnit7/TestCompatibilityTrait.php
new file mode 100644
index 0000000000..7f8300b57a
--- /dev/null
+++ b/core/tools/PhpUnit/PhpUnitCompatibility/PhpUnit7/TestCompatibilityTrait.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\Tool\PhpUnit\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/tools/PhpUnit/PhpUnitCompatibility/RunnerVersion.php b/core/tools/PhpUnit/PhpUnitCompatibility/RunnerVersion.php
new file mode 100644
index 0000000000..50b69dc2ab
--- /dev/null
+++ b/core/tools/PhpUnit/PhpUnitCompatibility/RunnerVersion.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Drupal\Tool\PhpUnit\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];
+  }
+
+}
