PHPUnit file structure, namespace, and required metadata

Last updated on
2 February 2024

File structure and namespace

There's a special relationship between the location of test files within the file structure, the namespace they use, and the class which they extend.

Drupal Unit Test Suites: Auto-detected Tests

Drupal 8 uses a number of test suites to allow for choosing between different kinds of tests to run.

Drupal 8 has the following test suites:

  • Simpletest: Legacy tests based on the Drupal version of the Simpletest framework.
  • Unit: PHPUnit-based tests with minimal dependencies.
  • Kernel: PHPUnit-based tests with a bootstrapped kernel, and a minimal number of extensions enabled.
  • Functional: PHPUnit-based tests with a booted Drupal instance.
  • FunctionalJavascript: PHPUnit-based tests that use Webdriver (PhantomJS before Drupal 8.5) to perform tests of Javascript functionality.

Drupal's test system can discover these different test suites for core, core extensions, and contrib extensions, as long as the tests are placed within the proper directories so they can be discovered as belonging to one of the test suites listed above.

Simpletest-based tests must be placed in an extension, within that extension's src/Tests directory. They will have a namespace of \Drupal\$extension\Tests.

PHPUnit-based: Generally, the other suites must be within a tests/src/$suite_type directory. These tests will have a namespace of \Drupal\Tests\$extension\$suite_type. Note that there are exceptions to this naming scheme, illustrated below.

Traits: The third class is traits. It is possible to write a trait that will be discovered and can be used by the various different test types. Each test suite will discover the trait if it is placed in the tests/src/Traits directory and the trait will have a namespace of \Drupal\Tests\$extension\Traits.

Note that the class names must end with the word 'Test'. The test methods within the class must start with the word 'test'. Like this:

class SomethingTest extends BrowserTestBase {
  public function testSomething() {
    // Your assertions here.
  }
}

Core Classes

Tests of core classes are placed in the following directory and their namespace will consist of the path minus the configured folder. Note that namespace uses backslashes, but otherwise matches the file structure.

Directory: core/tests/Drupal/Tests/Core/[component]
Namespace: Drupal\Tests\Core\[component]

Examples:

  • Test: core/tests/Drupal/Tests/Core/UrlTest.php
  • namespace Drupal\Tests\Core
  • class UrlTest extends UnitTestCase
  • Test: core/tests/Drupal/Tests/Core/Form/ConfirmFormHelperTest.php
  • namespace Drupal\Tests\Core\Form
  • class ConfirmFormHelperTest extends UnitTestCase
  • Test: core/tests/Drupal/Tests/Core/Entity/Controller/EntityViewControllerTest.php
  • namespace Drupal\Tests\Core\Entity\Controller
  • class EntityViewControllerTest extends UnitTestCase

Core Components

Directory: core/tests/Drupal/Tests/Component/[component]
Namespace: Drupal\Tests\Component\[component]

Example:

  • Test: core/tests/Drupal/Tests/Component/Datetime/DateTimePlusTest.php
  • namespace Drupal\Tests\Component\Datetime
  • class DateTimePlusTest extends UnitTestCase

Core Modules

Directory: core/modules/[modulename]/tests/src/Unit
Namespace: Drupal\Tests\[modulename]\Unit

Examples:

  • Test: core/modules/aggregator/tests/src/Unit/Plugin/AggregatorPluginSettingsBaseTest.php
  • namespace Drupal\Tests\aggregator\Unit\Plugin
  • class AggregatorPluginSettingsBaseTest extends UnitTestCase
  • Test: core/modules/views_ui/tests/src/Unit/Form/Ajax/RearrangeFilterTest.php
  • namespace Drupal\Tests\views_ui\Unit\Form\Ajax
  • class RearrangeFilterTest extends UnitTestCase

Contributed Modules

Contributed modules' tests will be discovered by the Drupal test runner in either the new Drupal 8 modules folder at the root of the Drupal installation or in the legacy /sites/*/modules folder. The test directory, file, namespace, and class must conform to the PSR-4 standard as in the following example.

Example:

  • A test located in any of the following:
    • modules/phpunit_example/tests/src/Unit/Double_DisplayManagerTest.php OR
    • sites/all/modules/phpunit_example/tests/src/Unit/Double_DisplayManagerTest.php
  • Must correspond with the following namespace and class:
    • namespace Drupal\Tests\phpunit_example\Unit
    • class Double_DisplayManagerTest extends UnitTestCase

Note that you may run PHPUnit directly using a custom configuration that does not conform to the Drupal test runner standard above. This will work locally and on other platforms.

Required PHPDoc metadata for test discoverability

Metadata about a test is done with native PHPDoc documentation on the test class itself, following PHPUnit's lead:

Examples

File: modules/ice_cream/tests/src/Unit/IceCreamTest.php

  • Name: Generated from the fully-qualified test class name.
  • Description: Generated from the PHPDoc summary line.
  • Group: Generated from the @group PHPDoc annotation.
    • Every test class MUST specify at least one (first) @group that matches the originating module short name (or Drupal core component name).
namespace Drupal\Tests\ice_cream\Unit;

/**
 * Tests generation of ice cream.
 *
 * @group ice_cream
 */
class IceCreamTest extends UnitTestCase {
  ...
}

The summary is unnecessary if using the @coversDefaultClass annotation:

/**
 * @coversDefaultClass \Drupal\ice_cream\IceCream
 * @group ice_cream
 */
class IceCreamTest extends UnitTestCase {
  ...
}

Mocking global functions

It is best practice to inject the service rather than the deprecated global function into classes. However, if a class does use a global function the following pattern needs to be followed in order for the test runner to work.

Example

This example is from the aggregator module, which uses drupal_set_message globally. Note that this does not work with namespace such as \drupal_set_message.

namespace Drupal\Tests\aggregator\Unit\Plugin;

class AggregatorPluginSettingsBaseTest extends UnitTestCase {}

namespace Drupal\Core\Form;

if (!function_exists('drupal_set_message')) {
  function drupal_set_message() {}
}

Note that as of Drupal 8.5 (#2278383), you can use an injectable service for drupal_set_message, but the above is still a useful example for other instances with global functions.

Help improve this page

Page status: No known problems

You can: