diff --git a/core/modules/migrate/tests/src/Unit/Entity/MigrationTest.php b/core/modules/migrate/tests/src/Unit/Entity/MigrationTest.php
index 027cccd..73ab716 100644
--- a/core/modules/migrate/tests/src/Unit/Entity/MigrationTest.php
+++ b/core/modules/migrate/tests/src/Unit/Entity/MigrationTest.php
@@ -14,6 +14,7 @@
  * Tests the migrate entity.
  *
  * @coversDefaultClass \Drupal\migrate\Entity\Migration
+ * @group migrate
  */
 class MigrationTest extends UnitTestCase {
 
diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module
index 052e0b7..1f16d10 100644
--- a/core/modules/simpletest/simpletest.module
+++ b/core/modules/simpletest/simpletest.module
@@ -144,7 +144,7 @@ function simpletest_run_tests($test_list) {
 
   // Get the info for the first test being run.
   $first_test = reset($test_list);
-  $info = TestDiscovery::getTestInfo(new \ReflectionClass($first_test));
+  $info = TestDiscovery::getTestInfo($first_test);
 
   $batch = array(
     'title' => t('Running tests'),
@@ -320,7 +320,7 @@ function _simpletest_batch_operation($test_list_init, $test_id, &$context) {
   $test = new $test_class($test_id);
   $test->run();
   $size = count($test_list);
-  $info = TestDiscovery::getTestInfo(new \ReflectionClass($test));
+  $info = TestDiscovery::getTestInfo($test_class);
 
   \Drupal::moduleHandler()->invokeAll('test_finished', array($test->results));
 
diff --git a/core/modules/simpletest/src/Exception/MissingGroupException.php b/core/modules/simpletest/src/Exception/MissingGroupException.php
new file mode 100644
index 0000000..e877b88
--- /dev/null
+++ b/core/modules/simpletest/src/Exception/MissingGroupException.php
@@ -0,0 +1,14 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\simpletest\Exception\MissingGroupException.
+ */
+
+namespace Drupal\simpletest\Exception;
+
+/**
+ * Exception thrown when a simpletest class is missing an @group annotation.
+ */
+class MissingGroupException extends \LogicException {
+}
diff --git a/core/modules/simpletest/src/Exception/MissingSummaryLineException.php b/core/modules/simpletest/src/Exception/MissingSummaryLineException.php
new file mode 100644
index 0000000..4c78815
--- /dev/null
+++ b/core/modules/simpletest/src/Exception/MissingSummaryLineException.php
@@ -0,0 +1,14 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\simpletest\Exception\MissingSummaryLineException.
+ */
+
+namespace Drupal\simpletest\Exception;
+
+/**
+ * Exception thrown when a simpletest class is missing a summary line.
+ */
+class MissingSummaryLineException extends \LogicException {
+}
diff --git a/core/modules/simpletest/src/Form/SimpletestResultsForm.php b/core/modules/simpletest/src/Form/SimpletestResultsForm.php
index a127683..c8366fb 100644
--- a/core/modules/simpletest/src/Form/SimpletestResultsForm.php
+++ b/core/modules/simpletest/src/Form/SimpletestResultsForm.php
@@ -153,7 +153,7 @@ public function buildForm(array $form, FormStateInterface $form_state, $test_id
     $form['result']['results'] = array();
     foreach ($results as $group => $assertions) {
       // Create group details with summary information.
-      $info = TestDiscovery::getTestInfo(new \ReflectionClass($group));
+      $info = TestDiscovery::getTestInfo($group);
       $form['result']['results'][$group] = array(
         '#type' => 'details',
         '#title' => $info['name'],
diff --git a/core/modules/simpletest/src/TestDiscovery.php b/core/modules/simpletest/src/TestDiscovery.php
index 50e19f0..68a2fed 100644
--- a/core/modules/simpletest/src/TestDiscovery.php
+++ b/core/modules/simpletest/src/TestDiscovery.php
@@ -8,9 +8,14 @@
 namespace Drupal\simpletest;
 
 use Composer\Autoload\ClassLoader;
+use Doctrine\Common\Annotations\SimpleAnnotationReader;
+use Doctrine\Common\Reflection\StaticReflectionParser;
+use Drupal\Component\Annotation\Reflection\MockFileFinder;
+use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Cache\CacheBackendInterface;
-use Drupal\Core\Extension\Extension;
 use Drupal\Core\Extension\ExtensionDiscovery;
+use Drupal\simpletest\Exception\MissingGroupException;
+use Drupal\simpletest\Exception\MissingSummaryLineException;
 use PHPUnit_Util_Test;
 
 /**
@@ -128,6 +133,9 @@ public function registerTestNamespaces() {
    * @todo Add base class groups 'Kernel' + 'Web', complementing 'PHPUnit'.
    */
   public function getTestClasses($extension = NULL) {
+    $reader = new SimpleAnnotationReader();
+    $reader->addNamespace('Drupal\\simpletest\\Annotation');
+
     if (!isset($extension)) {
       if ($this->cacheBackend && $cache = $this->cacheBackend->get('simpletest:discovery:classes')) {
         return $cache->data;
@@ -144,24 +152,16 @@ public function getTestClasses($extension = NULL) {
     $this->classLoader->addClassMap($classmap);
 
     foreach ($classmap as $classname => $pathname) {
+      $finder = MockFileFinder::create($pathname);
+      $parser = new StaticReflectionParser($classname, $finder, TRUE);
       try {
-        $class = new \ReflectionClass($classname);
-      }
-      catch (\ReflectionException $e) {
-        // Re-throw with expected pathname.
-        $message = $e->getMessage() . " in expected $pathname";
-        throw new \ReflectionException($message, $e->getCode(), $e);
-      }
-      // Skip interfaces, abstract classes, and traits.
-      if (!$class->isInstantiable()) {
-        continue;
+        $info = static::getTestInfo($classname, $parser->getDocComment());
       }
-      // Skip non-test classes.
-      if (!$class->isSubclassOf('Drupal\simpletest\TestBase') && !$class->isSubclassOf('PHPUnit_Framework_TestCase')) {
+      catch (\LogicException $e) {
+        // If the class is missing a summary line or an @group annotation just
+        // skip it.  Most likely it is an abstract class, trait or test fixture.
         continue;
       }
-      $info = static::getTestInfo($class);
-
       // Skip this test class if it requires unavailable modules.
       // @todo PHPUnit skips tests with unmet requirements when executing a test
       //   (instead of excluding them upfront). Refactor test runner to follow
@@ -275,8 +275,11 @@ public static function scanDirectory($namespace_prefix, $path) {
   /**
    * Retrieves information about a test class for UI purposes.
    *
-   * @param \ReflectionClass $class
-   *   The reflected test class.
+   * @param string $class
+   *   The test classname.
+   * @param string $doc_comment
+   *   (optional) The class PHPDoc comment. If not passed in reflection will be
+   *   used but this is very expensive when parsing all the test classes.
    *
    * @return array
    *   An associative array containing:
@@ -290,76 +293,52 @@ public static function scanDirectory($namespace_prefix, $path) {
    * @throws \LogicException
    *   If the class does not have a PHPDoc summary line or @coversDefaultClass
    *   annotation.
-   * @throws \LogicException
+   * @throws \Drupal\simpletest\Exception\MissingGroupException
    *   If the class does not have a @group annotation.
    */
-  public static function getTestInfo(\ReflectionClass $class) {
-    $classname = $class->getName();
+  public static function getTestInfo($classname, $doc_comment = NULL) {
+    if (!$doc_comment) {
+      $reflection = new \ReflectionClass($classname);
+      $doc_comment = $reflection->getDocComment();
+    }
     $info = array(
       'name' => $classname,
     );
-
-    // Automatically convert @coversDefaultClass into summary.
-    $annotations = static::parseTestClassAnnotations($class);
-    if (isset($annotations['coversDefaultClass'][0])) {
-      $info['description'] = 'Tests ' . $annotations['coversDefaultClass'][0] . '.';
-    }
-    elseif ($summary = static::parseTestClassSummary($class)) {
-      $info['description'] = $summary;
-    }
-    else {
-      throw new \LogicException(sprintf('Missing PHPDoc summary line on %s in %s.', $classname, $class->getFileName()));
-    }
-
-    // Reduce to @group and @requires.
-    $info += array_intersect_key($annotations, array('group' => 1, 'requires' => 1));
-
-    // @todo Remove legacy getInfo() methods.
-    if (method_exists($classname, 'getInfo')) {
-      $legacy_info = $classname::getInfo();
-
-      // Derive the primary @group from the namespace to ensure that legacy
-      // tests are not located in different groups than converted tests.
-      $classparts = explode('\\', $classname);
-      if ($classparts[1] === 'Tests') {
-        if ($classparts[2] === 'Component' || $classparts[2] === 'Core') {
-          // Drupal\Tests\Component\{group}\...
-          $info['group'][] = $classparts[3];
-        }
-        else {
-          // Drupal\Tests\{group}\...
-          $info['group'][] = $classparts[2];
+    $annotations = array();
+    preg_match_all('/^ \* \@([^\s]*) (.*$)/m', $doc_comment, $matches);
+    if (isset($matches[1])) {
+      foreach ($matches[1] as $key => $annotation) {
+        if (!empty($annotations[$annotation])) {
+          // Only have the first match per annotation. This deals with
+          // multiple @group annotations.
+          continue;
         }
+        $annotations[$annotation] = $matches[2][$key];
       }
-      elseif ($classparts[1] === 'system' && $classparts[3] !== 'System') {
-        // Drupal\system\Tests\{group}\...
-        $info['group'][] = $classparts[3];
-      }
-      else {
-        // Drupal\{group}\Tests\...
-        $info['group'][] = $classparts[1];
-      }
+    }
 
-      if (isset($legacy_info['dependencies'])) {
-        $info += array('requires' => array());
-        $info['requires'] += array('module' => array());
-        $info['requires']['module'] = array_merge($info['requires']['module'], $legacy_info['dependencies']);
-      }
+    if (empty($annotations['group'])) {
+      // Concrete tests must have a group.
+      throw new MissingGroupException(sprintf('Missing @group annotation in %s', $classname));
     }
+    // Standardise the group name.
+    $info['group'] = Unicode::ucfirst($annotations['group']);
 
-    // Process @group information.
-    // @todo Support multiple @groups + change UI to expose a group select
-    //   dropdown to filter tests by group instead of collapsible table rows.
-    // @see https://www.drupal.org/node/2296615
-    // @todo Replace single enforced PHPUnit group with base class groups.
-    if ($class->isSubclassOf('PHPUnit_Framework_TestCase')) {
-      $info['group'] = 'PHPUnit';
+    if (!empty($annotations['coversDefaultClass'])) {
+      $info['description'] = 'Tests ' . $annotations['coversDefaultClass'] . '.';
     }
     else {
-      if (empty($info['group'])) {
-        throw new \LogicException("Missing @group for $classname.");
+      // Some preg to strip annotations and comment headers.
+      preg_match('/^ \* ([^\@].*)$/m', $doc_comment, $matches);
+      if (isset($matches[1])) {
+        $info['description'] = $matches[1];
       }
-      $info['group'] = reset($info['group']);
+      else {
+        throw new MissingSummaryLineException(sprintf('Missing PHPDoc summary line in %s', $classname));
+      }
+    }
+    if (isset($annotations['dependencies'])) {
+      $info['requires']['module'] = array_map('trim', explode(',', $annotations['dependencies']));
     }
 
     return $info;
diff --git a/core/modules/simpletest/src/Tests/MissingDependentModuleUnitTest.php b/core/modules/simpletest/src/Tests/MissingDependentModuleUnitTest.php
index 956f280..a9e375c 100644
--- a/core/modules/simpletest/src/Tests/MissingDependentModuleUnitTest.php
+++ b/core/modules/simpletest/src/Tests/MissingDependentModuleUnitTest.php
@@ -13,7 +13,7 @@
  * This test should not load since it requires a module that is not found.
  *
  * @group simpletest
- * @requires module simpletest_missing_module
+ * @dependencies simpletest_missing_module
  */
 class MissingDependentModuleUnitTest extends KernelTestBase {
 
diff --git a/core/modules/simpletest/tests/src/Unit/TestInfoParsingTest.php b/core/modules/simpletest/tests/src/Unit/TestInfoParsingTest.php
new file mode 100644
index 0000000..65fe987
--- /dev/null
+++ b/core/modules/simpletest/tests/src/Unit/TestInfoParsingTest.php
@@ -0,0 +1,146 @@
+<?php
+
+namespace Drupal\Tests\simpletest\Unit;
+
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\simpletest\TestDiscovery
+ * @group simpletest
+ */
+class TestInfoParsingTest extends UnitTestCase {
+
+  /**
+   * @covers ::getTestInfo
+   * @dataProvider infoParserProvider
+   */
+  public function testTestInfoParser($expected, $classname, $doc_comment = NULL) {
+    $info = \Drupal\simpletest\TestDiscovery::getTestInfo($classname, $doc_comment);
+    $this->assertEquals($expected, $info);
+  }
+
+  public function infoParserProvider() {
+    $tests[] = [
+      // Expected result.
+      [
+        'name' => 'Drupal\Tests\simpletest\Unit\TestInfoParsingTest',
+        'group' => 'Simpletest',
+        'description' => 'Tests \Drupal\simpletest\TestDiscovery.',
+      ],
+      // Classname.
+      'Drupal\Tests\simpletest\Unit\TestInfoParsingTest',
+    ];
+
+    $tests[] = [
+      // Expected result.
+      [
+        'name' => 'Drupal\field\Tests\BulkDeleteTest',
+        'group' => 'Field',
+        'description' => 'Bulk delete storages and fields, and clean up afterwards.',
+      ],
+      // Classname.
+      'Drupal\field\Tests\BulkDeleteTest',
+      // Doc block.
+      "/**
+ * Bulk delete storages and fields, and clean up afterwards.
+ *
+ * @group field
+ */
+ ",
+    ];
+
+    // Multiple @group annotations.
+    $tests[] = [
+      // Expected result.
+      [
+        'name' => 'Drupal\field\Tests\BulkDeleteTest',
+        'group' => 'Test',
+        'description' => 'Bulk delete storages and fields, and clean up afterwards.',
+      ],
+      // Classname.
+      'Drupal\field\Tests\BulkDeleteTest',
+      // Doc block.
+      "/**
+ * Bulk delete storages and fields, and clean up afterwards.
+ *
+ * @group test
+ * @group field
+ */
+ ",
+    ];
+
+    // @dependencies annotation.
+    $tests[] = [
+      // Expected result.
+      [
+        'name' => 'Drupal\field\Tests\BulkDeleteTest',
+        'group' => 'Field',
+        'description' => 'Bulk delete storages and fields, and clean up afterwards.',
+        'requires' => ['module' => ['test']],
+      ],
+      // Classname.
+      'Drupal\field\Tests\BulkDeleteTest',
+      // Doc block.
+      "/**
+ * Bulk delete storages and fields, and clean up afterwards.
+ *
+ * @dependencies test
+ * @group field
+ */
+ ",
+    ];
+
+    // Multiple @dependencies annotation.
+    $tests[] = [
+      // Expected result.
+      [
+        'name' => 'Drupal\field\Tests\BulkDeleteTest',
+        'group' => 'Field',
+        'description' => 'Bulk delete storages and fields, and clean up afterwards.',
+        'requires' => ['module' => ['test', 'test1', 'test2']],
+      ],
+      // Classname.
+      'Drupal\field\Tests\BulkDeleteTest',
+      // Doc block.
+      "/**
+ * Bulk delete storages and fields, and clean up afterwards.
+ *
+ * @dependencies test, test1,test2
+ * @group field
+ */
+ ",
+    ];
+    return $tests;
+  }
+
+  /**
+   * @covers ::getTestInfo
+   * @expectedException \Drupal\simpletest\Exception\MissingGroupException
+   * @expectedExceptionMessage Missing @group annotation in Drupal\field\Tests\BulkDeleteTest
+   */
+  public function testTestInfoParserMissingGroup() {
+    $classname = 'Drupal\field\Tests\BulkDeleteTest';
+    $doc_comment = <<<EOT
+/**
+ * Bulk delete storages and fields, and clean up afterwards.
+ */
+EOT;
+    \Drupal\simpletest\TestDiscovery::getTestInfo($classname, $doc_comment);
+  }
+
+  /**
+   * @covers ::getTestInfo
+   * @expectedException \Drupal\simpletest\Exception\MissingSummaryLineException
+   * @expectedExceptionMessage Missing PHPDoc summary line in Drupal\field\Tests\BulkDeleteTest
+   */
+  public function testTestInfoParserMissingSummary() {
+    $classname = 'Drupal\field\Tests\BulkDeleteTest';
+    $doc_comment = <<<EOT
+/**
+ * @group field
+ */
+EOT;
+    \Drupal\simpletest\TestDiscovery::getTestInfo($classname, $doc_comment);
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Component/EventDispatcher/ContainerAwareEventDispatcherTest.php b/core/tests/Drupal/Tests/Component/EventDispatcher/ContainerAwareEventDispatcherTest.php
index b297626..58c11b0 100644
--- a/core/tests/Drupal/Tests/Component/EventDispatcher/ContainerAwareEventDispatcherTest.php
+++ b/core/tests/Drupal/Tests/Component/EventDispatcher/ContainerAwareEventDispatcherTest.php
@@ -26,6 +26,8 @@
  * synchronizations.
  *
  * @see https://github.com/symfony/symfony/pull/12521
+ *
+ * @group Symfony
  */
 class ContainerAwareEventDispatcherTest extends AbstractEventDispatcherTest
 {
diff --git a/core/tests/Drupal/Tests/Component/Plugin/DefaultFactoryTest.php b/core/tests/Drupal/Tests/Component/Plugin/DefaultFactoryTest.php
index 21dcf7c..0854ebe 100644
--- a/core/tests/Drupal/Tests/Component/Plugin/DefaultFactoryTest.php
+++ b/core/tests/Drupal/Tests/Component/Plugin/DefaultFactoryTest.php
@@ -12,6 +12,7 @@
 
 /**
  * @coversDefaultClass \Drupal\Component\Plugin\DefaultFactory
+ * @group Plugin
  */
 class DefaultFactoryTest extends UnitTestCase {
 
