Skip to main content
Version: v2

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:

# 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

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:

The Vortex CLI 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 6 environment types (local, ci, development, preview, stage, and production), 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:

  • EnvironmentSettingsTest covers environment detection and the settings each environment type gets.
  • SwitchableSettingsTest covers the settings that modules and services switch on and off.
  • DatabaseSettingsTest covers 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 Vortex CLI keeps this test only when you choose CircleCI at its Continuous Integration provider prompt. The test looks up the deploy and deploy-tags jobs of the commit workflow by name, so if you rename either job, 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 Vortex CLI 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 skipped group
  • 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:

VariableValuePurpose
SIMPLETEST_BASE_URLhttp://webserver:8080The site URL that functional tests request
SIMPLETEST_DBmysql://drupal:drupal@database/drupalThe database connection that kernel and functional tests use
MINK_DRIVER_CLASSEmptyLeaves each test base class on its default Mink driver
MINK_DRIVER_ARGSEmptyPasses no arguments to the default Mink driver
MINK_DRIVER_ARGS_WEBDRIVERChrome at http://browser:4444/wd/hubStarts the browser 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 bootstrap
  • kernel - tests with a partial Drupal bootstrap (database and core services)
  • functional - tests with a full Drupal bootstrap and browser simulation
  • functional-javascript - tests with a full Drupal bootstrap and a real browser (Chromium 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 Chromium browser running in the browser container, built from the selenium/standalone-chromium image. phpunit.xml passes the following flags to it in MINK_DRIVER_ARGS_WEBDRIVER, so the tests run the same way every time:

FlagPurpose
--disable-extensionsPrevents interference from browser extensions
--disable-popup-blockingAllows tests to open popups without being blocked
--disable-translatePrevents the translation bar from appearing on non-English pages
--force-prefers-reduced-motionDisables CSS animations and transitions for test stability
--test-typeSuppresses error dialogs and crash recovery prompts
--window-size=1920,1080Sets 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:

  1. Sets BROWSERTEST_OUTPUT_BASE_URL from the LOCALDEV_URL environment variable, adding http:// when the URL has no scheme, so that HTML output links work from the host machine.
  2. Creates the web/sites/simpletest/browser_output directory, which the HtmlOutputLogger extension needs before it initializes (see Drupal core issue #2992069).
  3. 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.php in custom modules and themes
  • *.deploy.php files in custom modules
  • node_modules directories in custom modules and themes
  • The project-level tests directory

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 Vortex CLI'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.