diff --git a/core/modules/views/tests/src/Kernel/ViewsKernelTestBase.php b/core/modules/views/tests/src/Kernel/ViewsKernelTestBase.php
index f53309bd040c53fe22d6dc9997b9bab8c03249d1..f149b7fba1fb826b743c52fcd6af07ad05c552a8 100644
--- a/core/modules/views/tests/src/Kernel/ViewsKernelTestBase.php
+++ b/core/modules/views/tests/src/Kernel/ViewsKernelTestBase.php
@@ -11,7 +11,7 @@
 /**
  * Defines a base class for Views kernel testing.
  */
-class ViewsKernelTestBase extends KernelTestBase {
+abstract class ViewsKernelTestBase extends KernelTestBase {
 
   use ViewResultAssertionTrait;
 
diff --git a/core/phpunit.xml.dist b/core/phpunit.xml.dist
index 2b45c6429fe0021fc8f23ec9b78977edd38abda3..951bd55d44b981e95add270c3d3b4edc8312ccb6 100644
--- a/core/phpunit.xml.dist
+++ b/core/phpunit.xml.dist
@@ -28,48 +28,16 @@
   </php>
   <testsuites>
     <testsuite name="unit">
-      <directory>./tests/Drupal/Tests</directory>
-      <directory>./modules/*/tests/src/Unit</directory>
-      <directory>../modules/*/tests/src/Unit</directory>
-      <directory>../profiles/*/tests/src/Unit</directory>
-      <directory>../sites/*/modules/*/tests/src/Unit</directory>
-      <!-- Exclude Composer's vendor directory so we don't run tests there. -->
-      <exclude>./vendor</exclude>
-      <!-- Exclude Drush tests. -->
-      <exclude>./drush/tests</exclude>
+      <file>./tests/TestSuites/UnitTestSuite.php</file>
     </testsuite>
     <testsuite name="kernel">
-      <directory>./tests/Drupal/KernelTests</directory>
-      <directory>./modules/*/tests/src/Kernel</directory>
-      <directory>../modules/*/tests/src/Kernel</directory>
-      <directory>../profiles/*/tests/src/Kernel</directory>
-      <directory>../sites/*/modules/*/tests/src/Kernel</directory>
-      <!-- Exclude Composer's vendor directory so we don't run tests there. -->
-      <exclude>./vendor</exclude>
-      <!-- Exclude Drush tests. -->
-      <exclude>./drush/tests</exclude>
+      <file>./tests/TestSuites/KernelTestSuite.php</file>
     </testsuite>
     <testsuite name="functional">
-      <directory>./tests/Drupal/FunctionalTests</directory>
-      <directory>./modules/*/tests/src/Functional</directory>
-      <directory>../modules/*/tests/src/Functional</directory>
-      <directory>../profiles/*/tests/src/Functional</directory>
-      <directory>../sites/*/modules/*/tests/src/Functional</directory>
-      <!-- Exclude Composer's vendor directory so we don't run tests there. -->
-      <exclude>./vendor</exclude>
-      <!-- Exclude Drush tests. -->
-      <exclude>./drush/tests</exclude>
+      <file>./tests/TestSuites/FunctionalTestSuite.php</file>
     </testsuite>
     <testsuite name="functional-javascript">
-      <directory>./tests/Drupal/FunctionalJavascriptTests</directory>
-      <directory>./modules/*/tests/src/FunctionalJavascript</directory>
-      <directory>../modules/*/tests/src/FunctionalJavascript</directory>
-      <directory>../profiles/*/tests/src/FunctionalJavascript</directory>
-      <directory>../sites/*/modules/*/tests/src/FunctionalJavascript</directory>
-      <!-- Exclude Composer's vendor directory so we don't run tests there. -->
-      <exclude>./vendor</exclude>
-      <!-- Exclude Drush tests. -->
-      <exclude>./drush/tests</exclude>
+      <file>./tests/TestSuites/FunctionalJavascriptTestSuite.php</file>
     </testsuite>
   </testsuites>
   <listeners>
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php b/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php
index 834d0a47b2a9284920afd50a506c7b47f9c7dce9..7c5e8557d3b4bfddcf0e2a776bb92f249a1a2b18 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php
@@ -22,7 +22,7 @@
 /**
  * Base class for the actual unit tests testing \Drupal\Core\Render\Renderer.
  */
-class RendererTestBase extends UnitTestCase {
+abstract class RendererTestBase extends UnitTestCase {
 
   /**
    * The tested renderer.
diff --git a/core/tests/Drupal/Tests/TestSuites/TestSuiteBaseTest.php b/core/tests/Drupal/Tests/TestSuites/TestSuiteBaseTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..bebb4310d074fdf54ec3ae9c4b14157d2350868f
--- /dev/null
+++ b/core/tests/Drupal/Tests/TestSuites/TestSuiteBaseTest.php
@@ -0,0 +1,134 @@
+<?php
+
+namespace Drupal\Tests\TestSuites;
+
+use org\bovigo\vfs\vfsStream;
+
+/**
+ * @coversDefaultClass \Drupal\Tests\TestSuites\TestSuiteBase
+ *
+ * @group TestSuite
+ */
+class TestSuiteBaseTest extends \PHPUnit_Framework_TestCase {
+
+  /**
+   * Helper method to set up the file system.
+   *
+   * @return array[]
+   *   A Drupal filesystem suitable for use with vfsStream.
+   */
+  protected function getFilesystem() {
+    return [
+      'core' => [
+        'modules' => [],
+        'profiles' => [],
+        'tests' => [
+          'Drupal' => [
+            'NotUnitTests' => [
+              'CoreNotUnitTest.php' => '<?php',
+            ],
+            'Tests' => [
+              'CoreUnitTest.php' => '<?php',
+            ],
+          ],
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * @return array[]
+   *   Test data for testAddTestsBySuiteNamespaceCore(). An array of arrays:
+   *   - A filesystem array for vfsStream.
+   *   - The sub-namespace of the test suite.
+   *   - The array of tests expected to be discovered.
+   */
+  public function provideCoreTests() {
+    $filesystem = $this->getFilesystem();
+    return [
+      'unit-tests' => [
+        $filesystem,
+        'Unit',
+        [
+          'Drupal\Tests\CoreUnitTest' => 'vfs://root/core/tests/Drupal/Tests/CoreUnitTest.php',
+        ],
+      ],
+      'not-unit-tests' => [
+        $filesystem,
+        'NotUnit',
+        [
+          'Drupal\NotUnitTests\CoreNotUnitTest' => 'vfs://root/core/tests/Drupal/NotUnitTests/CoreNotUnitTest.php',
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Tests for special case behavior of unit test suite namespaces in core.
+   *
+   * @covers ::addTestsBySuiteNamespace
+   *
+   * @dataProvider provideCoreTests
+   */
+  public function testAddTestsBySuiteNamespaceCore($filesystem, $suite_namespace, $expected_tests) {
+    // Set up the file system.
+    $vfs = vfsStream::setup('root');
+    vfsStream::create($filesystem, $vfs);
+
+    // Make a stub suite base to test.
+    $stub = new StubTestSuiteBase('test_me');
+
+    // Access addTestsBySuiteNamespace().
+    $ref_add_tests = new \ReflectionMethod($stub, 'addTestsBySuiteNamespace');
+    $ref_add_tests->setAccessible(TRUE);
+
+    // Invoke addTestsBySuiteNamespace().
+    $ref_add_tests->invokeArgs($stub, [vfsStream::url('root'), $suite_namespace]);
+
+    // Determine if we loaded the expected test files.
+    $this->assertNotEmpty($stub->testFiles);
+    $this->assertEmpty(array_diff_assoc($expected_tests, $stub->testFiles));
+  }
+
+}
+
+/**
+ * Stub subclass of TestSuiteBase.
+ *
+ * We use this class to alter the behavior of TestSuiteBase so it can be
+ * testable.
+ */
+class StubTestSuiteBase extends TestSuiteBase {
+
+  /**
+   * Test files discovered by addTestsBySuiteNamespace().
+   *
+   * @var string[]
+   */
+  public $testFiles = [];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function findExtensionDirectories($root) {
+    // We have to stub findExtensionDirectories() because we can't inject a
+    // vfsStream filesystem into drupal_phpunit_find_extension_directories(),
+    // which uses \SplFileInfo->getRealPath(). getRealPath() resolves
+    // stream-based paths to an empty string. See
+    // https://github.com/mikey179/vfsStream/wiki/Known-Issues
+    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/TestSuites/FunctionalJavascriptTestSuite.php b/core/tests/TestSuites/FunctionalJavascriptTestSuite.php
new file mode 100644
index 0000000000000000000000000000000000000000..626be021830eb384066fa0d972975d5c9af1eb9f
--- /dev/null
+++ b/core/tests/TestSuites/FunctionalJavascriptTestSuite.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\Tests\TestSuites;
+
+require_once __DIR__ . '/TestSuiteBase.php';
+
+/**
+ * Discovers tests for the functional-javascript test suite.
+ */
+class FunctionalJavascriptTestSuite extends TestSuiteBase {
+
+  /**
+   * Factory method which loads up a suite with all functional javascript tests.
+   *
+   * @return static
+   *   The test suite.
+   */
+  public static function suite() {
+    $root = dirname(dirname(dirname(__DIR__)));
+
+    $suite = new static('functional-javascript');
+    $suite->addTestsBySuiteNamespace($root, 'FunctionalJavascript');
+
+    return $suite;
+  }
+
+}
diff --git a/core/tests/TestSuites/FunctionalTestSuite.php b/core/tests/TestSuites/FunctionalTestSuite.php
new file mode 100644
index 0000000000000000000000000000000000000000..221a20daf9666ee13fd76de46b52fdadd5a379dd
--- /dev/null
+++ b/core/tests/TestSuites/FunctionalTestSuite.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\Tests\TestSuites;
+
+require_once __DIR__ . '/TestSuiteBase.php';
+
+/**
+ * Discovers tests for the functional test suite.
+ */
+class FunctionalTestSuite extends TestSuiteBase {
+
+  /**
+   * Factory method which loads up a suite with all functional tests.
+   *
+   * @return static
+   *   The test suite.
+   */
+  public static function suite() {
+    $root = dirname(dirname(dirname(__DIR__)));
+
+    $suite = new static('functional');
+    $suite->addTestsBySuiteNamespace($root, 'Functional');
+
+    return $suite;
+  }
+
+}
diff --git a/core/tests/TestSuites/KernelTestSuite.php b/core/tests/TestSuites/KernelTestSuite.php
new file mode 100644
index 0000000000000000000000000000000000000000..2625e2b1e24f560a0bcbc31d66277b1dd1094dd9
--- /dev/null
+++ b/core/tests/TestSuites/KernelTestSuite.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\Tests\TestSuites;
+
+require_once __DIR__ . '/TestSuiteBase.php';
+
+/**
+ * Discovers tests for the kernel test suite.
+ */
+class KernelTestSuite extends TestSuiteBase {
+
+  /**
+   * Factory method which loads up a suite with all kernel tests.
+   *
+   * @return static
+   *   The test suite.
+   */
+  public static function suite() {
+    $root = dirname(dirname(dirname(__DIR__)));
+
+    $suite = new static('kernel');
+    $suite->addTestsBySuiteNamespace($root, 'Kernel');
+
+    return $suite;
+  }
+
+}
diff --git a/core/tests/TestSuites/TestSuiteBase.php b/core/tests/TestSuites/TestSuiteBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..bd047e83c4d8cb0a339f2b48d912bc1e605fc70e
--- /dev/null
+++ b/core/tests/TestSuites/TestSuiteBase.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace Drupal\Tests\TestSuites;
+use Drupal\simpletest\TestDiscovery;
+
+/**
+ * Base class for Drupal test suites.
+ */
+abstract class TestSuiteBase extends \PHPUnit_Framework_TestSuite {
+
+  /**
+   * Finds extensions in a Drupal installation.
+   *
+   * An extension is defined as a directory with an *.info.yml file in it.
+   *
+   * @param string $root
+   *   Path to the root of the Drupal installation.
+   *
+   * @return string[]
+   *   Associative array of extension paths, with extension name as keys.
+   */
+  protected function findExtensionDirectories($root) {
+    $extension_roots = \drupal_phpunit_contrib_extension_directory_roots($root);
+
+    $extension_directories = array_map('drupal_phpunit_find_extension_directories', $extension_roots);
+    return array_reduce($extension_directories, 'array_merge', array());
+  }
+
+  /**
+   * Find and add tests to the suite for core and any extensions.
+   *
+   * @param string $root
+   *   Path to the root of the Drupal installation.
+   * @param string $suite_namespace
+   *   SubNamespace used to separate test suite. Examples: Unit, Functional.
+   */
+  protected function addTestsBySuiteNamespace($root, $suite_namespace) {
+    // Core's tests are in the namespace Drupal\${suite_namespace}Tests\ and are
+    // always inside of core/tests/Drupal/${suite_namespace}Tests. The exception
+    // to this is Unit tests for historical reasons.
+    if ($suite_namespace == 'Unit') {
+      $this->addTestFiles(TestDiscovery::scanDirectory("Drupal\\Tests\\", "$root/core/tests/Drupal/Tests"));
+    }
+    else {
+      $this->addTestFiles(TestDiscovery::scanDirectory("Drupal\\${suite_namespace}Tests\\", "$root/core/tests/Drupal/${suite_namespace}Tests"));
+    }
+
+    // Extensions' tests will always be in the namespace
+    // Drupal\Tests\$extension_name\$suite_namespace\ and be in the
+    // $extension_path/tests/src/$suite_namespace directory. Not all extensions
+    // will have all kinds of tests.
+    foreach ($this->findExtensionDirectories($root) as $extension_name => $dir) {
+      $test_path = "$dir/tests/src/$suite_namespace";
+      if (is_dir($test_path)) {
+        $this->addTestFiles(TestDiscovery::scanDirectory("Drupal\\Tests\\$extension_name\\$suite_namespace\\", $test_path));
+      }
+    }
+  }
+
+}
diff --git a/core/tests/TestSuites/UnitTestSuite.php b/core/tests/TestSuites/UnitTestSuite.php
new file mode 100644
index 0000000000000000000000000000000000000000..b05d455e473f51bd4d40e41113bdb392d3002d01
--- /dev/null
+++ b/core/tests/TestSuites/UnitTestSuite.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\Tests\TestSuites;
+
+require_once __DIR__ . '/TestSuiteBase.php';
+
+/**
+ * Discovers tests for the unit test suite.
+ */
+class UnitTestSuite extends TestSuiteBase {
+
+  /**
+   * Factory method which loads up a suite with all unit tests.
+   *
+   * @return static
+   *   The test suite.
+   */
+  public static function suite() {
+    $root = dirname(dirname(dirname(__DIR__)));
+
+    $suite = new static('unit');
+    $suite->addTestsBySuiteNamespace($root, 'Unit');
+
+    return $suite;
+  }
+
+}
diff --git a/core/tests/bootstrap.php b/core/tests/bootstrap.php
index 49e350aca75908b6fdfc9b004f065758af950e76..804aedf0bce79ee37a907d400c38c0fa95d00a71 100644
--- a/core/tests/bootstrap.php
+++ b/core/tests/bootstrap.php
@@ -23,7 +23,9 @@ function drupal_phpunit_find_extension_directories($scan_directory) {
   $dirs = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($scan_directory, \RecursiveDirectoryIterator::FOLLOW_SYMLINKS));
   foreach ($dirs as $dir) {
     if (strpos($dir->getPathname(), '.info.yml') !== FALSE) {
-      // Cut off ".info.yml" from the filename for use as the extension name.
+      // Cut off ".info.yml" from the filename for use as the extension name. We
+      // use getRealPath() so that we can scan extensions represented by
+      // directory aliases.
       $extensions[substr($dir->getFilename(), 0, -9)] = $dir->getPathInfo()
         ->getRealPath();
     }
@@ -34,11 +36,16 @@ function drupal_phpunit_find_extension_directories($scan_directory) {
 /**
  * Returns directories under which contributed extensions may exist.
  *
+ * @param string $root
+ *   (optional) Path to the root of the Drupal installation.
+ *
  * @return array
  *   An array of directories under which contributed extensions may exist.
  */
-function drupal_phpunit_contrib_extension_directory_roots() {
-  $root = dirname(dirname(__DIR__));
+function drupal_phpunit_contrib_extension_directory_roots($root = NULL) {
+  if ($root === NULL) {
+    $root = dirname(dirname(__DIR__));
+  }
   $paths = array(
     $root . '/core/modules',
     $root . '/core/profiles',