Skip to main content

Testing

Follow this page to run your extension's tests against the site you built, all at once or a few at a time. It also shows where their coverage and output go.

With Make or Ahoy, test starts the browser for FunctionalJavascript tests, then runs every PHPUnit suite and the Jest tests. It stops at the first command that fails, so Jest runs only after PHPUnit passes. To run every test, run the following command:

make test

ahoy test running the PHPUnit suites and the Jest tests in a terminal

The PHPUnit tests live in tests/src, in a directory per suite. The installer gives a module a working example test in each suite, mostly for its service, settings form and hooks. A theme gets 2 examples: a Unit test of its .theme hook and a FunctionalJavascript test with the theme as default. To run the tests of an extension you bundle with yours, see Bundled extensions.

phpunit.xml configures PHPUnit, and phpunit.d10.xml replaces it in a Drupal 10 build. Both bootstrap Drupal with core's web/core/tests/bootstrap.php and fail a run on a PHPUnit warning. Only a Drupal 10 build reads phpunit.d10.xml, so the installer removes it when you don't select Drupal 10. assemble copies phpunit.xml and jest.config.js into build, and in a Drupal 10 build it writes phpunit.d10.xml over build/phpunit.xml. Run assemble again after you edit any of these files.

Run 1 suite​

Each suite also has a command of its own:

make test-unit # Run Unit tests
make test-kernel # Run Kernel tests
make test-functional # Run Functional tests
make test-functional-javascript # Run FunctionalJavascript tests
make test-javascript # Run JavaScript unit tests (Jest)

Without a wrapper, FunctionalJavascript tests need the browser commands that their page shows. The JavaScript tests run with npm test in build. To save the pages that Functional and FunctionalJavascript tests visit, also export BROWSERTEST_OUTPUT_DIRECTORY before cd build (see Test output).

The following table lists the suites and what each one needs:

SuiteDirectoryNeeds
Unittests/src/UnitNothing beyond the codebase.
Kerneltests/src/KernelA database, which the tests install themselves.
Functionaltests/src/FunctionalThe running webserver.
FunctionalJavascripttests/src/FunctionalJavascriptThe running webserver and a browser. See FunctionalJavascript tests.

Functional and FunctionalJavascript tests load their pages from the webserver that build leaves running. stop and assemble stop the webserver, so after either one, run start before these tests (see Build the site).

Run specific tests​

Pass a test file, or PHPUnit options such as --filter or --group, to a test command. PHPUnit runs inside build, so a test path is relative to build. The path starts with your extension's directory in the assembled site: web/modules/custom/<your_extension> for a module, or web/themes/custom/<your_extension> for a theme:

make test-unit web/modules/custom/your_extension/tests/src/Unit/MyUnitTest.php
make test-unit -- --group=wip
make test-unit -- --filter=MyUnitTest::testSomething

make reads the command line before the test command does, so follow 3 rules with make:

  • Put -- before the arguments when they include an option, or make reads the option as its own.
  • Keep spaces and make target names, such as test, out of an argument, because make runs a target name as a target of its own. An option value joined with = can hold either, as in --filter='my test' or --filter=test.
  • Repeat an option with a space, as in --group wip --group slow. make keeps only 1 value per option joined with =.

JavaScript tests​

Jest runs the *.test.js files in the js directory of your extension with the configuration in jest.config.js. The tests run in jsdom, a simulated browser environment. To run them, run the following command:

make test-javascript

test-javascript is also available as test-js. Jest runs from the frontend dependencies that the build installs. When your project has a .skip_npm_build file, the build skips them and Jest doesn't run (see Frontend dependencies).

Code coverage​

PHPUnit collects code coverage with pcov, and Jest with V8. Each tool writes an HTML report to browse and a Cobertura file for other tools. They go to the phpunit and jest subdirectories of .logs/coverage, in the root of your project.

CI uploads the reports as a workflow artifact, and to Codecov when the CODECOV_TOKEN repository secret is set. The installer removes both upload steps with PHPUnit, so a project that keeps Jest but drops PHPUnit uploads no coverage.

The PHPUnit coverage counts your extension's .php files, plus a module's .module and .install files or a theme's .theme file. It leaves out the tests directory, rector.php and node_modules.

Functional and FunctionalJavascript tests reach your code through page requests, which run in the webserver's process. The coverage comes only from the PHPUnit process, so those requests add none. Code that runs while a test installs the site, such as hook_install(), still counts. To count the rest, the example Kernel tests build and submit the settings form and call the hooks themselves. The Functional test runs the same code through the browser and adds no coverage.

Deprecated code​

How a deprecation affects a run depends on the Drupal major:

  • In a Drupal 10 build, Symfony's PHPUnit Bridge handles deprecations, and SYMFONY_DEPRECATIONS_HELPER decides whether they fail the run.
  • In Drupal 11 and 12 builds, PHPUnit reports deprecations without failing the run. A PHP deprecation that the webserver raises during a Functional test still fails that test, because Drupal sends it back to the test.

To ignore some deprecations, list them in a file of your own:

  1. Add a .deprecation-ignore.txt file to the root of your project.
  2. In phpunit.xml, uncomment the line that sets SYMFONY_DEPRECATIONS_HELPER to ignoreFile=../.deprecation-ignore.txt. PHPUnit runs from the build directory, so the path is relative to build.
  3. If your project has phpunit.d10.xml, uncomment the same line there.
  4. Run assemble to copy the changed files into build.

The file holds 1 regular expression per line, and a line that starts with # is a comment. A deprecation whose message matches a line is ignored. A Drupal 10 build doesn't count it toward failing the run, and Drupal 11 and 12 builds leave it out of the report. While SYMFONY_DEPRECATIONS_HELPER is unset, core ignores the deprecations that its own web/core/.deprecation-ignore.txt lists. Your file replaces that list. See the change record for details.

CI test jobs on PHP 8.4 or newer set SYMFONY_DEPRECATIONS_HELPER to disabled. To match your project's deprecation policy, change that step of .github/workflows/test.yml.

When SYMFONY_DEPRECATIONS_HELPER is disabled in the environment that runs assemble, as in those CI jobs, assemble turns off PHP deprecation notices in 3 files of the build:

  • The start of the site's index.php
  • sites/default/default.settings.php
  • Core's scaffold copy, core/assets/scaffold/files/default.settings.php, which the sites that Functional tests install copy their settings.php from

So in those jobs, a PHP deprecation that the webserver raises doesn't fail a Functional test. assemble also patches a function signature in the PHP 8.3 polyfill that PHP 8.4 reports as deprecated.

Test output​

Functional and FunctionalJavascript tests save the HTML of every page they visit to .logs/browser_output. A FunctionalJavascript test that calls createAutoScreenshot(), as the example smoke test does, saves its screenshots there too. After the test progress, the run prints "HTML output was generated." and a URL for each saved page. The PHPUnit summary follows. CI uploads the directory as a workflow artifact, so a failure in CI comes with the pages that led to it.

The pages land there after provision links Drupal's output directory into .logs, and only while BROWSERTEST_OUTPUT_DIRECTORY is set. make, Ahoy and CI set it. Without a wrapper, export the variable before cd build:

export BROWSERTEST_OUTPUT_DIRECTORY="$PWD/.logs/browser_output"