class PhpUnitTestDiscovery

Discovers available tests using the PHPUnit API.

@internal

Hierarchy

Expanded class hierarchy of PhpUnitTestDiscovery

2 files declare their use of PhpUnitTestDiscovery
PhpUnitApiFindAllClassFilesTest.php in core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiFindAllClassFilesTest.php
PhpUnitApiGetTestClassesTest.php in core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiGetTestClassesTest.php

File

core/lib/Drupal/Core/Test/PhpUnitTestDiscovery.php, line 21

Namespace

Drupal\Core\Test
View source
class PhpUnitTestDiscovery {
    
    /**
     * The map of legacy test suite identifiers to phpunit.xml ones.
     *
     * @var array<string,string>
     */
    private array $map = [
        'PHPUnit-FunctionalJavascript' => 'functional-javascript',
        'PHPUnit-Functional' => 'functional',
        'PHPUnit-Kernel' => 'kernel',
        'PHPUnit-Unit' => 'unit',
        'PHPUnit-Unit-Component' => 'unit-component',
        'PHPUnit-Build' => 'build',
    ];
    
    /**
     * The reverse map of legacy test suite identifiers to phpunit.xml ones.
     *
     * @var array<string,string>
     */
    private array $reverseMap;
    
    /**
     * The warnings generated during the discovery.
     *
     * @var list<string>
     */
    private array $warnings = [];
    public function __construct(string $configurationFilePath) {
        $this->reverseMap = array_flip($this->map);
    }
    
    /**
     * Discovers available tests.
     *
     * @param string|null $extension
     *   (optional) The name of an extension to limit discovery to; e.g., 'node'.
     * @param list<string> $testSuites
     *   (optional) An array of PHPUnit test suites to filter the discovery for.
     * @param string|null $directory
     *   (optional) Limit discovered tests to a specific directory.
     *
     * @return array<string<array<class-string, array{name: class-string, description: string, group: string|int, groups: list<string|int>, type: string, file: string, tests_count: positive-int}>>>
     *   An array of test groups keyed by the group name. Each test group is an
     *   array of test class information arrays as returned by
     *   ::getTestClassInfo(), keyed by test class. If a test class belongs to
     *   multiple groups, it will appear under all group keys it belongs to.
     */
    public function getTestClasses(?string $extension = NULL, array $testSuites = [], ?string $directory = NULL) : array {
        $this->warnings = [];
        $args = [
            '--configuration',
            $this->configurationFilePath,
        ];
        if (!empty($testSuites)) {
            // Convert $testSuites from Drupal's legacy syntax to the syntax used in
            // phpunit.xml, that is necessary to PHPUnit to be able to apply the
            // test suite filter. For example, 'PHPUnit-Unit' to 'unit'.
            $tmp = [];
            foreach ($testSuites as $i) {
                if (!is_string($i)) {
                    throw new \InvalidArgumentException("Test suite must be a string");
                }
                if (str_contains($i, ' ')) {
                    throw new \InvalidArgumentException("Test suite name '{$i}' is invalid");
                }
                $tmp[] = $this->map[$i] ?? $i;
            }
            $args[] = '--testsuite=' . implode(',', $tmp);
        }
        if ($directory !== NULL) {
            $args[] = $directory;
        }
        $phpUnitConfiguration = (new Builder())->build($args);
        // TestSuiteBuilder calls the test data providers during the discovery.
        // Data providers may be changing the Drupal service container, which leads
        // to potential issues. We save the current container before running the
        // discovery, and in case a change is detected, reset it and raise
        // warnings so that developers can tune their data provider code.
        if (\Drupal::hasContainer()) {
            $container = \Drupal::getContainer();
            $containerObjectId = spl_object_id($container);
        }
        $phpUnitTestSuite = (new TestSuiteBuilder())->build($phpUnitConfiguration);
        if (isset($containerObjectId) && $containerObjectId !== spl_object_id(\Drupal::getContainer())) {
            $this->warnings[] = '*** The service container was changed during the test discovery ***';
            $this->warnings[] = 'Probably a test data provider method called \\Drupal::setContainer.';
            $this->warnings[] = 'Ensure that all the data providers restore the original container before returning data.';
            assert(isset($container));
            \Drupal::setContainer($container);
        }
        $list = $directory === NULL ? $this->getTestList($phpUnitTestSuite, $extension) : $this->getTestListLimitedToDirectory($phpUnitTestSuite, $extension, $testSuites);
        // Sort the groups and tests within the groups by name.
        uksort($list, 'strnatcasecmp');
        foreach ($list as &$tests) {
            uksort($tests, 'strnatcasecmp');
        }
        return $list;
    }
    
    /**
     * Discovers all class files in all available extensions.
     *
     * @param string|null $extension
     *   (optional) The name of an extension to limit discovery to; e.g., 'node'.
     * @param string|null $directory
     *   (optional) Limit discovered tests to a specific directory.
     *
     * @return array
     *   A classmap containing all discovered class files; i.e., a map of
     *   fully-qualified classnames to path names.
     */
    public function findAllClassFiles(?string $extension = NULL, ?string $directory = NULL) : array {
        $testClasses = $this->getTestClasses($extension, [], $directory);
        $classMap = [];
        foreach ($testClasses as $group) {
            foreach ($group as $className => $info) {
                $classMap[$className] = $info['file'];
            }
        }
        return $classMap;
    }
    
    /**
     * Returns the warnings generated during the discovery.
     *
     * @return list<string>
     *   The warnings.
     */
    public function getWarnings() : array {
        return $this->warnings;
    }
    
    /**
     * Returns a list of tests from a TestSuite object.
     *
     * @param \PHPUnit\Framework\TestSuite $phpUnitTestSuite
     *   The TestSuite object returned by PHPUnit test discovery.
     * @param string|null $extension
     *   The name of an extension to limit discovery to; e.g., 'node'.
     *
     * @return array<string<array<class-string, array{name: class-string, description: string, group: string|int, groups: list<string|int>, type: string, file: string, tests_count: positive-int}>>>
     *   An array of test groups keyed by the group name. Each test group is an
     *   array of test class information arrays as returned by
     *   ::getTestClassInfo(), keyed by test class. If a test class belongs to
     *   multiple groups, it will appear under all group keys it belongs to.
     */
    private function getTestList(TestSuite $phpUnitTestSuite, ?string $extension) : array {
        $list = [];
        foreach ($phpUnitTestSuite->tests() as $testSuite) {
            foreach ($testSuite->tests() as $testClass) {
                if ($extension !== NULL && !str_starts_with($testClass->name(), "Drupal\\Tests\\{$extension}\\")) {
                    continue;
                }
                $item = $this->getTestClassInfo($testClass, $this->reverseMap[$testSuite->name()] ?? $testSuite->name());
                foreach ($item['groups'] as $group) {
                    $list[$group][$item['name']] = $item;
                }
            }
        }
        return $list;
    }
    
    /**
     * Returns a list of tests from a TestSuite object limited to a directory.
     *
     * @param \PHPUnit\Framework\TestSuite $phpUnitTestSuite
     *   The TestSuite object returned by PHPUnit test discovery.
     * @param string|null $extension
     *   The name of an extension to limit discovery to; e.g., 'node'.
     * @param list<string> $testSuites
     *   An array of PHPUnit test suites to filter the discovery for.
     *
     * @return array<string<array<class-string, array{name: class-string, description: string, group: string|int, groups: list<string|int>, type: string, file: string, tests_count: positive-int}>>>
     *   An array of test groups keyed by the group name. Each test group is an
     *   array of test class information arrays as returned by
     *   ::getTestClassInfo(), keyed by test class. If a test class belongs to
     *   multiple groups, it will appear under all group keys it belongs to.
     */
    private function getTestListLimitedToDirectory(TestSuite $phpUnitTestSuite, ?string $extension, array $testSuites) : array {
        $list = [];
        // In this case, PHPUnit found a single test class to run tests for.
        if ($phpUnitTestSuite->isForTestClass()) {
            if ($extension !== NULL && !str_starts_with($phpUnitTestSuite->name(), "Drupal\\Tests\\{$extension}\\")) {
                return [];
            }
            // Take the test suite name from the class namespace.
            $testSuite = 'PHPUnit-' . TestDiscovery::getPhpunitTestSuite($phpUnitTestSuite->name());
            if (!empty($testSuites) && !in_array($testSuite, $testSuites, TRUE)) {
                return [];
            }
            $item = $this->getTestClassInfo($phpUnitTestSuite, $testSuite);
            foreach ($item['groups'] as $group) {
                $list[$group][$item['name']] = $item;
            }
            return $list;
        }
        // Multiple test classes were found.
        $list = [];
        foreach ($phpUnitTestSuite->tests() as $testClass) {
            if ($extension !== NULL && !str_starts_with($testClass->name(), "Drupal\\Tests\\{$extension}\\")) {
                continue;
            }
            // Take the test suite name from the class namespace.
            $testSuite = 'PHPUnit-' . TestDiscovery::getPhpunitTestSuite($testClass->name());
            if (!empty($testSuites) && !in_array($testSuite, $testSuites, TRUE)) {
                continue;
            }
            $item = $this->getTestClassInfo($testClass, $testSuite);
            foreach ($item['groups'] as $group) {
                $list[$group][$item['name']] = $item;
            }
        }
        return $list;
    }
    
    /**
     * Returns the test class information.
     *
     * @param \PHPUnit\Framework\Test $testClass
     *   The test class.
     * @param string $testSuite
     *   The test suite of this test class.
     *
     * @return array{name: class-string, description: string, group: string|int, groups: list<string|int>, type: string, file: string, tests_count: positive-int}
     *   The test class information.
     */
    private function getTestClassInfo(Test $testClass, string $testSuite) : array {
        $reflection = new \ReflectionClass($testClass->name());
        // Let PHPUnit API return the groups, as it will deal transparently with
        // annotations or attributes, but skip groups generated by PHPUnit
        // internally and starting with a double underscore prefix.
        if (RunnerVersion::getMajor() < 11) {
            $groups = array_filter($testClass->groups(), function (string $value) : bool {
                return !str_starts_with($value, '__phpunit');
            });
        }
        else {
            // In PHPUnit 11+, we need to coalesce the groups from individual tests
            // as they may not be available from the test class level (when tests are
            // backed by data providers).
            $tmp = [];
            foreach ($testClass as $test) {
                if ($test instanceof DataProviderTestSuite) {
                    foreach ($test as $testWithData) {
                        $tmp = array_merge($tmp, $testWithData->groups());
                    }
                }
                else {
                    $tmp = array_merge($tmp, $test->groups());
                }
            }
            $groups = array_filter(array_unique($tmp), function (string $value) : bool {
                return !str_starts_with($value, '__phpunit');
            });
        }
        if (empty($groups)) {
            throw new MissingGroupException(sprintf('Missing group metadata in test class %s', $testClass->name()));
        }
        // Let PHPUnit API return the class coverage information.
        $test = $testClass;
        while (!$test instanceof TestCase) {
            $test = $test->tests()[0];
        }
        if (($metadata = $test->valueObjectForEvents()
            ->metadata()
            ->isCoversClass()) && $metadata->isNotEmpty()) {
            $description = sprintf('Tests %s.', $metadata->asArray()[0]
                ->className());
        }
        elseif (($metadata = $test->valueObjectForEvents()
            ->metadata()
            ->isCoversDefaultClass()) && $metadata->isNotEmpty()) {
            $description = sprintf('Tests %s.', $metadata->asArray()[0]
                ->className());
        }
        else {
            $description = TestDiscovery::parseTestClassSummary($reflection->getDocComment());
        }
        // Find the test cases count.
        $count = 0;
        foreach ($testClass->tests() as $testCase) {
            if ($testCase instanceof TestCase) {
                // If it's a straight test method, counts 1.
                $count++;
            }
            else {
                // It's a data provider test suite, count 1 per data set provided.
                $count += count($testCase->tests());
            }
        }
        return [
            'name' => $testClass->name(),
            'group' => $groups[0],
            'groups' => $groups,
            'type' => $testSuite,
            'description' => $description,
            'file' => $reflection->getFileName(),
            'tests_count' => $count,
        ];
    }

}

Members

Title Sort descending Modifiers Object type Summary
PhpUnitTestDiscovery::$map private property The map of legacy test suite identifiers to phpunit.xml ones.
PhpUnitTestDiscovery::$reverseMap private property The reverse map of legacy test suite identifiers to phpunit.xml ones.
PhpUnitTestDiscovery::$warnings private property The warnings generated during the discovery.
PhpUnitTestDiscovery::findAllClassFiles public function Discovers all class files in all available extensions.
PhpUnitTestDiscovery::getTestClasses public function Discovers available tests.
PhpUnitTestDiscovery::getTestClassInfo private function Returns the test class information.
PhpUnitTestDiscovery::getTestList private function Returns a list of tests from a TestSuite object.
PhpUnitTestDiscovery::getTestListLimitedToDirectory private function Returns a list of tests from a TestSuite object limited to a directory.
PhpUnitTestDiscovery::getWarnings public function Returns the warnings generated during the discovery.
PhpUnitTestDiscovery::__construct public function

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.