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
- Ahoy
- No wrapper
make test
ahoy test
vendor/bin/eddy-browser-start
export WEBDRIVER_PORT="$(vendor/bin/eddy-info webdriver-port)"
export WEBDRIVER_BACKEND="$(vendor/bin/eddy-info webdriver-backend)"
export WEBDRIVER_HOST="$(vendor/bin/eddy-info webdriver-host)"
export WEBSERVER_PORT="$(vendor/bin/eddy-info webserver-port)"
export SIMPLETEST_BASE_URL="http://localhost:${WEBSERVER_PORT}"
cd build
php -d pcov.directory=.. vendor/bin/phpunit
npm test
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
- Ahoy
- No wrapper
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)
ahoy test-unit # Run Unit tests
ahoy test-kernel # Run Kernel tests
ahoy test-functional # Run Functional tests
ahoy test-functional-javascript # Run FunctionalJavascript tests
ahoy test-javascript # Run JavaScript unit tests (Jest)
export WEBSERVER_PORT="$(vendor/bin/eddy-info webserver-port)"
export SIMPLETEST_BASE_URL="http://localhost:${WEBSERVER_PORT}"
cd build
php -d pcov.directory=.. vendor/bin/phpunit --testsuite unit
php -d pcov.directory=.. vendor/bin/phpunit --testsuite kernel
php -d pcov.directory=.. vendor/bin/phpunit --testsuite functional
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:
| Suite | Directory | Needs |
|---|---|---|
| Unit | tests/src/Unit | Nothing beyond the codebase. |
| Kernel | tests/src/Kernel | A database, which the tests install themselves. |
| Functional | tests/src/Functional | The running webserver. |
| FunctionalJavascript | tests/src/FunctionalJavascript | The 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
- Ahoy
- No wrapper
make test-unit web/modules/custom/your_extension/tests/src/Unit/MyUnitTest.php
make test-unit -- --group=wip
make test-unit -- --filter=MyUnitTest::testSomething
ahoy test-unit web/modules/custom/your_extension/tests/src/Unit/MyUnitTest.php
ahoy test-unit -- --group=wip
ahoy test-unit -- --filter=MyUnitTest::testSomething
cd build
php -d pcov.directory=.. vendor/bin/phpunit --testsuite unit \
web/modules/custom/your_extension/tests/src/Unit/MyUnitTest.php
php -d pcov.directory=.. vendor/bin/phpunit --testsuite unit --group=wip
php -d pcov.directory=.. vendor/bin/phpunit --testsuite 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, ormakereads the option as its own. - Keep spaces and
maketarget names, such astest, out of an argument, becausemakeruns 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.makekeeps 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
- Ahoy
- No wrapper
make test-javascript
ahoy test-javascript
cd build
npm test
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_HELPERdecides 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:
- Add a
.deprecation-ignore.txtfile to the root of your project. - In
phpunit.xml, uncomment the line that setsSYMFONY_DEPRECATIONS_HELPERtoignoreFile=../.deprecation-ignore.txt. PHPUnit runs from thebuilddirectory, so the path is relative tobuild. - If your project has
phpunit.d10.xml, uncomment the same line there. - Run
assembleto copy the changed files intobuild.
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 theirsettings.phpfrom
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"