How to Automate Testing for Drupal Websites
Build a reliable Drupal test workflow with PHPUnit, the right test layers, and GitLab CI. Set up local and browser tests, diagnose common failures, and add screenshot checks where they help.
Automate Drupal testing by matching each behavior to the narrowest PHPUnit test layer that can verify it, then run those tests in CI when code changes. Use unit tests for isolated PHP logic, kernel tests for behavior that needs Drupal services or entities, functional tests for complete site workflows, and FunctionalJavascript tests when real browser JavaScript behavior matters. For Drupal.org projects, configure GitLab CI in a root-level .gitlab-ci.yml, starting from the Drupal Association-maintained template. Drupal’s PHPUnit documentation and testing guide describe the layers and setup.
1. Choose the right Drupal test layer
| Layer | What it covers | Use it for | Needs |
|---|---|---|---|
| Unit | Isolated PHP logic with minimal Drupal runtime | Value objects, calculations, validation rules, and many input combinations | PHPUnit and project test dependencies |
| Kernel | A bootstrapped Drupal kernel with selected extensions | Services, entities, plugins, and request behavior that need Drupal but not a full site | Database configuration for applicable tests |
| Functional | A full Drupal instance through BrowserTestBase | Routes, forms, permissions, and workflows that do not require real JavaScript execution | Database, test site setup, and project configuration |
| FunctionalJavascript | Drupal in a real browser driven through WebDriver | AJAX, browser events, and client-side behavior | Database, reachable web server, browser, and compatible driver |
Prefer the least expensive layer that actually exercises the behavior. A unit test cannot prove that routing and permissions work in a Drupal site. A functional test does not prove an AJAX interaction occurred in a real browser. Conversely, use a non-JavaScript layer when browser interaction is not part of the behavior: Drupal notes that FunctionalJavascript tests need more tooling and take longer. See the official test types overview and FunctionalJavascript guide.
2. Inventory behavior and define what CI should protect
- List important module and theme behavior: pure logic, Drupal runtime behavior, complete user workflows, and browser-only interactions.
- Assign each case to the narrowest layer above that can detect a regression.
- Prioritize tests around changed code and high-risk behavior, such as access checks, data changes, and user-submitted forms.
- Decide which checks should run on every change and which browser-heavy checks can run in a separate job or broader branch workflow.
- Record supported PHP, Drupal core, database, and browser environments from the project’s actual dependency constraints. Do not copy version pins from an old example without checking compatibility.
This test mix is a practical design choice based on the differing scopes and prerequisites in Drupal’s documentation; it is not a prescribed universal matrix.
3. Install dependencies and configure PHPUnit
For Composer-based recommended projects, Drupal documents drupal/core-dev as a development dependency. Git-based checkouts need their Composer dependencies installed as well. Keep development-only dependencies off production servers. Configuration varies by project layout and core branch, so use the project’s current PHPUnit configuration rather than copying paths blindly.
# From the Drupal project root, install the project's declared dependencies.
composer install
# For a Composer project that needs Drupal's development dependencies,
# add the version compatible with the project's Drupal core constraints.
composer require --dev drupal/core-dev:^VERSION
Replace ^VERSION with a constraint supported by the project’s Drupal core branch and Composer setup; there is no safe universal version to paste. Consult the official PHPUnit setup guide for the current branch-specific commands and configuration details.
Before running tests, check the project’s PHPUnit XML configuration for:
- The Drupal bootstrap file and test discovery paths.
- A base URL and database connection for test types that require them.
- A writable directory for browser-test output.
- Correct paths for the project layout. Depending on layout, module or site-module tests may be run from Drupal’s
coredirectory with the vendor PHPUnit executable.
Drupal warns that core updates can overwrite core/phpunit.xml. Keep project-specific configuration in the location intended by the project and its current Drupal documentation, then review it after core updates.
4. Run tests locally before adding CI
Use the project’s configured PHPUnit executable and XML file. A common Composer project invocation is:
# From the Drupal project root; adjust the config path to this project's layout.
./vendor/bin/phpunit -c web/core/phpunit.xml web/modules/custom/example/tests/src/Unit
For a project where Drupal is at the repository root, the configuration or test path may instead use core/phpunit.xml and modules/custom/.... The key is to run the intended test path with the configuration that bootstraps that project. Use verbose output when diagnosing discovery or skip issues:
./vendor/bin/phpunit -v -c web/core/phpunit.xml web/modules/custom/example/tests/src
For applicable kernel or browser tests, ensure the test database is configured. Browser tests also require Drupal to be reachable through a web server. For FunctionalJavascript tests, start a compatible Chrome or Chromium and ChromeDriver/WebDriver service, then invoke PHPUnit directly. Drupal cautions against routing JavaScript tests through core/scripts/run-tests.sh when ChromeDriver may not be running. A command that exits successfully is not enough evidence that a test ran: inspect output for skipped tests and verify the intended test count.
5. Add GitLab CI for Drupal.org projects
Drupal.org project automation uses GitLab CI configured in .gitlab-ci.yml at the repository root. Drupal’s current guidance recommends beginning with the Drupal Association-maintained template, then adapting its test types and environment to the project. See Drupal’s GitLab CI documentation.
- Start with the maintained template linked from Drupal’s current GitLab CI documentation.
- Keep the configuration at the repository root and make sure dependency installation matches the project’s Composer files.
- Enable the test layers the project actually uses. Include database services for relevant kernel and functional tests, and a reachable test server plus browser tooling for FunctionalJavascript.
- Set the PHP, database, and Drupal core environments according to the versions the project supports.
- Run fast tests frequently and place slower browser tests in an appropriate CI job or trigger.
- Review any
.distconfiguration files. Drupal’s documentation warns that GitLab CI may consume configuration that DrupalCI previously ignored. - Inspect job logs to confirm tests were discovered and executed, not skipped because of missing services or configuration.
DrupalCI-specific automation guidance is retired; use the current GitLab CI documentation for Drupal.org projects. For other hosting platforms, translate the same dependency and service requirements into that platform’s pipeline configuration rather than assuming Drupal.org’s template applies unchanged.
6. Add visual checks for rendered pages
PHPUnit verifies code and behavior at its test layer. A screenshot can add a visual review artifact for rendered pages, such as a key route after a theme change. It does not replace assertions for access, data, or interactions. If capturing a page as part of a release or review workflow, keep the target stable, wait for meaningful content to appear, and avoid treating a screenshot mismatch alone as proof of the cause.
7. Troubleshoot common failures
| Symptom | Likely cause | What to check or fix |
|---|---|---|
| No tests found, or fewer tests than expected | Wrong test path, PHPUnit configuration, bootstrap, or project root | Check the configured test suites and paths; run with verbose output and use the project’s actual Drupal root layout. |
| Kernel or functional test cannot connect to the database | Missing or incorrect test database settings, or CI database service unavailable | Check the PHPUnit environment variables/configuration and ensure the CI job starts and waits for its database service. |
| Browser test cannot reach the site | No web server, incorrect base URL, or service hostname mismatch in CI | Start the test web server, set the test base URL to a reachable address, and check networking from the job container. |
| FunctionalJavascript test fails to start a browser | Chrome/Chromium or ChromeDriver is absent, unreachable, or incompatible | Install a matching browser driver, verify the WebDriver endpoint, and use the direct PHPUnit invocation prescribed by Drupal. |
| Tests are skipped unexpectedly | Missing environmental requirements or test annotations/conditions | Read the skip message and satisfy the documented dependency; do not count skips as passing coverage. |
| Tests pass locally but fail in CI | Different PHP/dependency versions, missing database or browser service, permissions, or assumptions about local state | Compare CI and local versions/configuration; make required services explicit and ensure generated output directories are writable. |
| Tests change behavior after a core update | Core update altered or replaced PHPUnit configuration | Review the effective XML configuration after updating core and restore project-specific settings in the intended location. |
8. Performance, reliability, and cost
- Performance: Keep pure logic in unit tests where possible, reserve full-site tests for site behavior, and use FunctionalJavascript only when browser behavior matters. The layers have different runtime and tooling costs; the dossier provides no universal duration figures.
- Reliability: Make databases, web servers, and browser drivers explicit CI dependencies. A green job is meaningful only if logs show the expected tests ran rather than being skipped.
- Maintenance: Keep dependencies in Composer manifests, check supported versions before changing CI matrices, and review configuration after core updates. Templates and compatibility constraints can change.
- Cost: CI compute usage depends on the hosting service, job frequency, and browser-test workload. No cost figures are specified here; use the provider’s current pricing and your actual pipeline usage.
9. Or skip the browser setup
For page screenshots without configuring a browser runner, ScreenshotNeo is a website screenshot API and MCP server for developers. A single GET request returns an image or PDF. See the ScreenshotNeo API documentation for options and setup.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
- Cookie and consent banners are accepted and removed before capture; newsletter popups and chat widgets are removed too. Each cleanup step can be turned off.
- Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing; response headers identify the page verdict and billing status.
- An MCP server lets AI agents, including Claude and Cursor, take screenshots with tools for screenshots, page information, and PDF capture.
- 1,000 screenshots per month are free with no card. Paid plans start at $5 for 3,000 shots; every feature is on every plan.
Sign up for ScreenshotNeo’s free 1,000 monthly screenshots with no card required.
10. Frequently asked questions
Should every Drupal module have all four test types?
No. Select layers from the behavior and dependencies under test; a module may have no need for real-browser tests if its behavior does not involve JavaScript interactions.
Can a screenshot prove a Drupal form or access check works?
No. A screenshot shows rendered output. Use PHPUnit assertions in a suitable test layer for form submission, permissions, and data behavior.
Is the old DrupalCI setup still the right starting point?
No for current Drupal.org project guidance: Drupal documents GitLab CI and points maintainers to its community-maintained template.
Can I use a single PHP version in CI?
Use the PHP versions supported by the project’s Drupal core branch and dependencies. Confirm compatibility from the project constraints and current Drupal guidance before defining a matrix.


