PHPUnit
PHPUnit tests your custom PHP code, and this page is for the developers who write those tests on a project built from Vortex. It shows how to run any suite or a single test, what the shipped configuration enforces, and which results can fail a build in continuous integration (CI). To unit-test JavaScript behaviors instead, see Jest.
Vortex sets PHPUnit up with 4 test suites that follow Drupal's test types. Unit tests check a class in isolation, kernel and functional tests check it against a bootstrapped Drupal, and functional JavaScript tests drive a real browser.
Vortex includes a PHPUnit configuration in
phpunit.xml,
based on Drupal core's core/phpunit.xml.dist, so you can customize the
test suites per project.
Usage
Run the tests with Ahoy, or with Docker Compose directly:
- Ahoy
- Docker Compose
# Run all PHPUnit tests (all suites)
ahoy test
# Run only Unit tests
ahoy test-unit
# Run only Kernel tests
ahoy test-kernel
# Run only Functional tests
ahoy test-functional
# Run only Functional JavaScript tests
ahoy test-functional-javascript
# Run specific test file
ahoy test tests/phpunit/ExampleTest.php
# Run tests with filter
ahoy test -- --filter=testMethodName
# Run tests in a group, set with the #[Group('group_name')] attribute
ahoy test -- --group=group_name
# Run all PHPUnit tests (all suites)
docker compose exec cli vendor/bin/phpunit
# Run only Unit tests
docker compose exec cli vendor/bin/phpunit --testsuite=unit
# Run only Kernel tests
docker compose exec cli vendor/bin/phpunit --testsuite=kernel
# Run only Functional tests
docker compose exec cli vendor/bin/phpunit --testsuite=functional
# Run only Functional JavaScript tests
docker compose exec cli vendor/bin/phpunit \
--testsuite=functional-javascript
# Run specific test file
docker compose exec cli vendor/bin/phpunit tests/phpunit/ExampleTest.php
# Run tests with filter
docker compose exec cli vendor/bin/phpunit --filter=testMethodName
# Run tests in a group, set with the #[Group('group_name')] attribute
docker compose exec cli vendor/bin/phpunit --group=group_name
Writing tests
Put each test in the tests/src/ directory of its custom module or theme,
in the Unit, Kernel, Functional, or FunctionalJavascript
subdirectory, so the matching test suite collects it.
Skipping tests
phpunit.xml excludes the skipped group by default, so a
#[Group('skipped')] attribute on a test class or method takes it out of
the test run. Import the attribute with
use PHPUnit\Framework\Attributes\Group; at the top of the test file, then
add it:
#[Group('skipped')]
public function testSomethingToSkip(): void {
// This test will be skipped
}
Calling markTestSkipped() instead fails the run, because phpunit.xml
sets failOnSkipped. Use the attribute rather than the older
@group skipped docblock annotation, too. PHPUnit 11 still reads that
docblock and leaves the test out, but it reports a deprecation, which
failOnPhpunitDeprecation turns into a failed run. On a method that
carries any attribute, PHPUnit ignores the docblock, so the test runs.
Boilerplate
Vortex ships the following boilerplate tests. They run in the continuous integration pipeline from the moment you install Vortex, and they make a good starting point for your own:
- Base custom module tests: unit, kernel, functional, and functional JavaScript tests
- Custom theme tests: unit, kernel, functional, and functional JavaScript tests
- Demo custom module tests: unit, kernel, and functional JavaScript tests of its counter block
- Project-level tests: the Drupal settings tests and the CI configuration tests, described in the following sections
The installer removes the tests of a module you deselect at its Custom modules prompt. It removes the custom theme's tests when you choose Olivero, Claro, or Stark at its Theme prompt.
Drupal settings tests
The Drupal settings
tests
check that the site gets the right settings for the environment type it
runs in. Every module's settings multiply against 5 environment types
(local, ci, dev, stage, and prod), so a missed override is a
common mistake. It shows up only after you deploy to the environment that
lacks it.
Each test class covers 1 area:
EnvironmentSettingsTestcovers environment detection and the settings each environment type gets.SwitchableSettingsTestcovers the settings that modules and services switch on and off.DatabaseSettingsTestcovers the database connection.
The tests belong to your site: keep them in step with every change you make
to settings.php and the files it includes.
Continuous integration pipeline configuration tests
The CircleCI configuration tests check the CircleCI configuration that's hardest to eyeball: the regular expressions that filter the branches and tags before they're deployed to the hosting environment. They match each filter against sample branch and tag names, and check that no filter pattern exceeds the 128 characters the CircleCI compiler accepts.
The installer keeps this test only when you choose CircleCI at its
Continuous Integration provider prompt. The test looks the deployment
jobs up by their position in the commit workflow, so if you move a job in
that list, update the test too. Keep the test in step with your continuous
integration configuration.
Project conventions
Your project's own PHPUnit conventions live in its docs/testing.md file,
which your project team maintains to record the testing practices you agree
on. It covers the following topics:
- Test class structure
- Base test classes
- Test data
- Data providers
- Kernel tests for services
Vortex ships the file in the project's docs/ directory, and the
installer keeps it unless you decline its
Preserve project documentation? prompt. Declining it removes the whole
docs/ directory.
Configuration
See the PHPUnit configuration reference.
All global configuration takes place in the
phpunit.xml
file in the project root. It configures the following:
- Test suite directories
- Bootstrap file for Drupal integration
- Environment variables for test execution
- Strictness settings that fail or stop the run
- Code coverage settings
- JUnit report
- Excluded
skippedgroup - HTML output logger for browser tests
The configuration is strict. A warning, a skipped test, or a PHPUnit deprecation fails the run, and the run stops at the first error, failure, warning, or risky test.
The following table lists the environment variables that phpunit.xml sets
for the tests:
| Variable | Value | Purpose |
|---|---|---|
SIMPLETEST_BASE_URL | http://nginx:8080 | The site URL that functional tests request |
SIMPLETEST_DB | mysql://drupal:drupal@database/drupal | The database connection that kernel and functional tests use |
MINK_DRIVER_CLASS | Empty | Leaves each test base class on its default Mink driver |
MINK_DRIVER_ARGS | Empty | Passes no arguments to the default Mink driver |
MINK_DRIVER_ARGS_WEBDRIVER | Chrome at http://chrome:4444/wd/hub | Starts Chrome for functional JavaScript tests |
By default, PHPUnit runs the tests of your custom modules and themes, plus
the project-level tests in tests/phpunit: the Drupal settings tests and,
on CircleCI projects, the CircleCI configuration test.
Test suites
phpunit.xml defines the following test suites:
unit- fast tests of isolated PHP classes without a Drupal bootstrapkernel- tests with a partial Drupal bootstrap (database and core services)functional- tests with a full Drupal bootstrap and browser simulationfunctional-javascript- tests with a full Drupal bootstrap and a real browser (Chrome through Selenium) for JavaScript interactions
Each suite collects the matching tests/src/ directory (Unit, Kernel,
Functional, or FunctionalJavascript) of every custom module and theme.
The unit suite also collects tests/phpunit.
To add test targets, add a directory to a test suite:
<testsuite name="unit">
<directory>my/custom/dir/*/tests</directory>
</testsuite>
Chrome session flags
Functional JavaScript tests use a Chrome browser running in the
selenium/standalone-chromium
container. phpunit.xml passes the following flags to it in
MINK_DRIVER_ARGS_WEBDRIVER, so the tests run the same way every time:
| Flag | Purpose |
|---|---|
--disable-extensions | Prevents interference from browser extensions |
--disable-popup-blocking | Allows tests to open popups without being blocked |
--disable-translate | Prevents the translation bar from appearing on non-English pages |
--force-prefers-reduced-motion | Disables CSS animations and transitions for test stability |
--test-type | Suppresses error dialogs and crash recovery prompts |
--window-size=1920,1080 | Sets a deterministic viewport size for consistent screenshots |
The w3c: true option is required for compatibility with Selenium 4.
Bootstrap
The custom
tests/phpunit/bootstrap.php
file runs before the tests and does the following, in order:
- Sets
BROWSERTEST_OUTPUT_BASE_URLfrom theLOCALDEV_URLenvironment variable, addinghttp://when the URL has no scheme, so that HTML output links work from the host machine. - Creates the
web/sites/simpletest/browser_outputdirectory, which theHtmlOutputLoggerextension needs before it initializes (see Drupal core issue #2992069). - Loads Drupal core's own
web/core/tests/bootstrap.php.
Reports
PHPUnit writes a JUnit XML report to
.logs/test_results/phpunit/phpunit.xml.
Coverage
PHPUnit writes 3 coverage reports to .logs/coverage/phpunit/ on every
run, because the CLI container image enables the PCOV coverage driver:
- A Cobertura XML report in
cobertura.xml, for automated coverage checks - A text summary in
coverage.txt - An HTML report in
.coverage-html, for browsing while you write tests
CI reads the coverage rate from cobertura.xml and builds the pull request
(PR) comment from coverage.txt.
Coverage measures the following code:
- Your custom modules and themes
- The settings files in
web/sites/default/includes web/sites/default/settings.php
Coverage leaves out the following files:
- Files ending in
Test.phpin custom modules and themes *.deploy.phpfiles in custom modulesnode_modulesdirectories in custom modules and themes- The project-level
testsdirectory
Files that no test runs still count, at 0%, because phpunit.xml sets
includeUncoveredFiles.
Ignoring lines from coverage
Sometimes you need to leave code out of coverage. For example, when testing
a module that uses a third-party library, you don't need to test the library
itself. phpunit.xml sets disableCodeCoverageIgnore to false, so the
following annotations take effect.
To ignore a method from coverage, add a @codeCoverageIgnore annotation to
the method docblock:
/**
* @codeCoverageIgnore
*/
public function myMethod() {
// ...
}
To ignore a block of lines from coverage, add @codeCoverageIgnoreStart
before the first line and @codeCoverageIgnoreEnd after the last one:
// @codeCoverageIgnoreStart
$a = 1;
$b = 2;
// @codeCoverageIgnoreEnd
Continuous integration
In the continuous integration pipeline, PHPUnit runs on the first container only (see Test parallelism). It runs with coverage by default, and the pipeline stores the coverage reports as artifacts. The pipeline also collects the JUnit report to track test performance and stability: GitHub Actions publishes it as test results, and CircleCI stores it with the job's test results.
If you chose Codecov at the installer's Code coverage provider
prompt, the pipeline also uploads the whole .logs/coverage directory to
Codecov when CODECOV_TOKEN is set. The upload includes the Jest coverage
report. CircleCI skips the whole upload on deps/* branches, while GitHub
Actions uploads on every build.
Coverage threshold
CI fails the build when line coverage drops below a minimum percentage. To
change the minimum, set VORTEX_CI_CODE_COVERAGE_THRESHOLD to a whole
number (default: 90), as a repository variable in GitHub Actions or an
environment variable in CircleCI.
The check reads the line rate from cobertura.xml. When the rate is too
low, the job log shows a line such as
FAIL: coverage 85.50% is below threshold 90%. The check has no
ignore-failure variable of its own, so it still runs, and can still fail the
build, when you ignore PHPUnit failures.
On pull requests, CI posts the coverage summary as a PR comment, with the threshold and a collapsible per-class breakdown. Each new report replaces the previous one: older comments are minimized to keep the PR timeline clean. GitHub Actions and CircleCI tag their comments with separate hidden markers, so each provider minimizes only its own comments.
CircleCI posts the comment only when a GITHUB_TOKEN environment variable
is set. Without one, the step prints GITHUB_TOKEN is not set. Skipping.
and moves on. GitHub Actions also writes the coverage percentage to the job
summary.
To turn the comment off, set VORTEX_CI_CODE_COVERAGE_PR_COMMENT_SKIP to
1, as a repository variable in GitHub Actions or an environment variable
in CircleCI.
Ignoring failures
Set VORTEX_CI_PHPUNIT_IGNORE_FAILURE to 1 to let the build pass despite
failing tests. PHPUnit still runs and reports the failure in the job log,
and the coverage threshold check still applies. See
Ignore tool
failures.