Automated testing for Drupal projects

Automated testing for Drupal projects covers custom module and theme code, critical user journeys, static analysis and CI gates so upgrades stay predictable. This page explains the layers, trade-offs, setup and what to confirm.

On this page

Why automated testing for Drupal projects keeps upgrades cheap

Automated testing on a Drupal project means running repeatable checks against your custom modules, themes and critical user journeys on every change, so a core or contributed update shows you what broke in minutes instead of in a production review. It becomes necessary as soon as custom code is expected to survive more than one update cycle, which for most teams is the first time a contributed module changes its hooks or a core API is deprecated. Without tests, every upgrade is a manual regression exercise, and the cost of that exercise grows with the amount of custom code you have written.

The test layers divide that work. Unit tests cover pure PHP logic with no Drupal bootstrap, kernel tests boot a minimal Drupal environment to exercise services, entities and access control, and functional tests run full page requests through the test browser. Browser tests drive a real browser against the rendered site and are best reserved for a small set of critical journeys such as checkout, login or a form submission, because they are the slowest and most fragile layer. Static analysis with PHPStan complements these by finding type errors and calls to removed methods without executing code. Your team should decide which layer owns each risk, and confirm the current PHPUnit and PHPStan versions and their Drupal integration against the documentation on Drupal.org before standardizing on them.

CI is what turns these checks into a gate. Each merge request should run the unit, kernel and functional suites, PHPStan and the coding standards checks, and block the merge on failure, with browser tests run on a schedule or before release if their runtime is high. Ask your development partner or internal team to show you the pipeline configuration, the runtime of each stage, and how test data and databases are provisioned, since flaky tests quietly erode trust in the gate. Plan the test suite as a maintained asset: someone must own the tests, update them when contributed modules change, and review failures before the next scheduled Drupal core release window.

Which test layer fits which code

Unit tests exercise a single class or function in isolation, with dependencies replaced by test doubles. In Drupal projects they suit pure logic: value transformations, pricing or eligibility rules, string formatting and small helper classes that do not touch the database, the container or the render system. Because they do not boot Drupal, they run in milliseconds and give the fastest feedback on every commit.

  • Speed: Unit tests are the cheapest layer to run on every push, so they carry most of the logic checks.
  • Brittleness: Tests that mock many services often break on refactors without finding real defects, so keep doubles limited to the boundary.
  • Blind spots: They cannot prove that services are wired correctly in the container, that config schema is valid or that entity queries return what you expect.
  • Suits: Custom calculation services, value objects and data mappers that sit behind a clear interface.

Check that each unit test asserts behavior rather than implementation details, so an upgrade of a core dependency does not require rewriting the whole suite.

How to set up Drupal test suites in CI

This sequence takes a Drupal project from version pinning to a pipeline where tests and static analysis block merges. Each step ends with a check that proves the layer works, so failures in CI mean something. The result is a suite your team can extend without rebuilding the pipeline.

  1. Pin PHP and database versions to the target Drupal release

    Start by confirming which Drupal core version the project targets, then match the PHP version to that release. According to the Drupal.org PHP requirements page, Drupal 10 requires PHP 8.1 or later, Drupal 11 requires PHP 8.3 or later and Drupal 12 requires PHP 8.5 or later. Choose the database your production site uses (MariaDB, MySQL, PostgreSQL or SQLite, per the Drupal.org system requirements page) and run CI against that same engine. Verification: a CI job that runs drush status on a fresh install reports the expected PHP version and database driver. If it does not, fix the image before writing any tests.

  2. Configure PHPUnit and the test suites

    Add a phpunit.xml file to the custom module or theme repository and define separate suites for unit, kernel and functional tests. Unit tests run without bootstrapping Drupal, kernel tests exercise services and entity storage against a real container, and functional tests drive a browser-level request. Keep each suite in its own directory so CI can run them independently and report failures by layer. Verification: add one deliberately failing assertion to a unit test, run the unit suite locally and in CI, and confirm the job fails with the expected message. Remove the assertion afterward.

  3. Add PHPStan at an agreed level with a baseline

    Install PHPStan with the Drupal extension and agree with the team on an analysis level that the custom code can meet today, then raise it over time. For existing code that already fails the level, generate a baseline file so CI only rejects new problems. Commit the baseline and record who approves changes to it. Verification: introduce a type error in a custom service, run PHPStan, and confirm it reports the error at the agreed level and not only in the baseline. Revert the change before continuing.

  4. Add browser test tooling and a seeded database

    Install the browser test driver the team has chosen and configure it to run against a Drupal instance built from the same code as the pipeline. Create a seeded database with a repeatable install profile, configuration import and fixture content for the critical user journeys, such as login, checkout or content approval. Store the seed as a database dump or a script that rebuilds it in the job. Verification: run the same journey twice in a row from a clean seed and confirm both runs pass with the same content counts, which shows the seed is deterministic.

  5. Wire the pipeline stages

    Define pipeline stages in the repository's CI configuration in this order: install dependencies with Composer, run static analysis, run unit and kernel tests, then run functional and browser tests against the seeded database. Cache Composer dependencies and keep the slow browser stage separate so failures in faster stages stop it early. Store test artifacts such as screenshots and JUnit reports for failed browser runs. Verification: push a branch with a failing kernel test and confirm the pipeline stops before the browser stage and publishes the report.

  6. Set required checks on the default branch

    In the Git hosting platform, mark the static analysis, unit, kernel and browser jobs as required status checks on the protected default branch, and require reviews before merge. Confirm with your platform's documentation how required checks are named and how they handle skipped jobs, since a skipped job may count as passing. Verification: open a merge request with a failing test and confirm the merge button is blocked. Then confirm a passing branch can merge.

  7. Document ownership and the upgrade path

    Write a short guide in the repository covering how to run each suite locally, how to update the PHPStan baseline, how to refresh the seed, and who approves changes to any of them. Include a step for running the suites against the next Drupal minor or major version in a separate branch before an upgrade, so deprecations surface early. Verification: a developer who did not build the pipeline runs every suite locally by following the guide alone, and the results match CI.

Configuration options and when each fits

This table compares the main configuration choices for a Drupal test suite: what runs on every merge, what runs nightly, how strict static analysis is, and how the test database is built. Read each row for when the option fits, what it costs in run time or fidelity, and who should own it. The main takeaway is that fast checks belong on every merge, while slower browser and database-fidelity checks are worth running on a schedule the team can actually watch.

OptionWhen it fitsTrade-off in run time or fidelityOwner
Unit tests on every mergePure PHP logic in services, plugins and value objects with no bootstrapped Drupal containerVery fast, but misses wiring errors between services, hooks and configModule developers
Kernel tests on every mergeEntity access, services, config schema and hooks that need a partial Drupal bootstrapModerate run time; catches integration faults that unit tests cannot seeModule developers with a senior reviewer
Functional and browser tests nightlyCritical user journeys such as checkout, login, form submission and editorial publishingSlowest tier and most prone to flakiness; results are delayed until the nightly runQA lead or a named test owner
PHPStan at a strict level with a baselineLegacy code where existing errors are recorded and only new errors fail the buildBaseline hides old defects, so it needs periodic reduction; a level set too high blocks mergesTech lead, with the baseline reviewed each sprint
SQLite for test databasesFast local runs and most kernel tests that do not depend on database-specific behaviorFaster setup, but collation, JSON and transaction behavior can differ from productionPlatform or CI engineer
Production database engine for test databasesSuites that touch queries, migrations, or behavior tied to MariaDB, MySQL or PostgreSQLHigher fidelity with slower provisioning; confirm the engine version matches productionPlatform or CI engineer
Parallel test executionLarge suites where the slowest tier is the bottleneck and tests are isolated from each otherShorter wall-clock time, but shared state and fixtures can cause intermittent failuresCI engineer, with test authors fixing isolation issues

What goes wrong with Drupal test suites

Most Drupal test suites fail for a small set of reasons: unstable browser checks, tests tied to presentation, missing coverage of custom code, and gates that people learn to ignore. Each failure below has a cause that is usually visible early and a fix that is usually a design decision, not more test volume.

Flaky browser tests

Browser tests that pass on one run and fail on the next usually depend on timing, shared test data or leftover state from earlier tests. Teams then rerun the pipeline until it goes green, and real regressions get lost in the noise. The fix is to keep browser tests few and focused on critical journeys, give each test its own data, and wait for explicit page conditions instead of fixed delays. Check whether failures reproduce locally and whether the suite shares one database between tests.

Tests coupled to markup

Assertions that match exact Twig output, CSS class names or wrapper elements break whenever a designer restyles a component, even when behavior is unchanged. This is expensive on component-based themes, where markup changes often. The fix is to assert on behavior the user or a service depends on, such as text content, form outcomes, access results or returned data, and to use stable test hooks where markup must be checked. Review failing tests after a theme change to see which assertions were about looks rather than function.

No tests on custom services

Teams often test the pages built on top of custom code but not the services, plugins and entity logic underneath. When a page test fails, the cause is hard to isolate, and upgrades to Drupal core or contributed modules can change behavior without anyone noticing. The fix is to put business rules in services with unit or kernel tests that run without a full site install, and to keep functional tests for the integration points. Ask which custom classes carry business rules and whether each has a direct test.

PHPStan baselines that hide errors

A PHPStan baseline records existing errors so the analysis can pass on a legacy codebase. When the baseline is regenerated carelessly or grows each time someone is in a hurry, new errors are added to it silently and the static check stops catching problems in new code. The fix is to freeze the baseline, fail CI when its entries increase, and fix errors in files you touch. Review the baseline in code review like any other change, and ask who approves additions.

Tests not run on the Drupal version in use

A suite that passes on the version a team wrote it against can fail after a core or contributed module update, and the failure may appear only in the next upgrade cycle. Gaps appear when CI pins one core release while production runs another, or when PHP versions in CI differ from those in hosting. The fix is to run the suite against the core and PHP versions you actually deploy, and to add a scheduled job against the next major version early enough to act on its findings. Confirm the matrix against the release schedule and PHP requirements for your release.

Slow suites that get bypassed

When a full run takes too long, developers push with skipped tests, run only part of the suite, or merge without waiting for results. The suite then protects less each month without anyone deciding that. The fix is to split the suite into fast unit and kernel tests that run on every change, and slower functional and browser tests that run on merge or on a schedule, with parallel execution where the runner supports it. Track how long each stage takes and treat growth as a defect to fix.

Questions teams ask before automating Drupal tests

What drives the cost of building a Drupal test suite?

The cost follows the amount of custom code and the number of critical user journeys that need coverage, not the size of the Drupal site as a whole. Contributed modules that you configure but do not modify usually need fewer tests of your own. The main cost components are analysis of which behavior matters most, writing unit, kernel and functional tests for custom modules and themes, setting up browser tests for checkout, login or publishing flows, and wiring PHPStan and CI. Ask any partner to break the quote into those components. One-time costs cover the initial suite and CI setup. Recurring costs cover updating tests when code changes and when Drupal core or contributed modules release new versions. Costs rise when the codebase has little existing structure, when external services must be stubbed, or when many browsers and environments are in scope.

How long does it take to set up automated tests for a Drupal project?

Duration depends on the same drivers as cost: the volume of custom code, the number of journeys to cover, how testable the existing code is, and how quickly the client team can answer questions. Code that uses services and dependency injection is easier to test than code that relies on global functions and static calls. A project with a clean CI pipeline and a stable staging environment will move faster than one where the pipeline must be built first. Because most engagements are fixed-scope, the timeline is agreed in writing before work starts, based on the scope. Ask for the milestones, which suites are delivered at each one, and what has to be ready for each milestone to close.

What does the client team need to provide for a test automation project?

The work depends on several inputs from the client team:

  • Test data: realistic but anonymized content, users with each role, and sample configuration that matches production. Personal data should not be copied into test environments without approval.
  • Environments: a stable, reachable staging or CI environment with the same PHP version, database engine and web server as production, plus credentials for CI runners.
  • Access: repository access, permission to change the CI configuration, and a named contact who can approve changes to shared pipelines.
  • Decisions: which journeys are business-critical, what coverage level is acceptable, and who signs off on failing tests blocking a release.

Without these decisions, the suite tends to test what is easy rather than what is important.

Can an in-house team build and maintain Drupal tests themselves?

Yes, if the team has PHP and Drupal experience and time to learn PHPUnit, the Drupal test base classes, and browser testing tools. Drupal's own test infrastructure covers unit, kernel and functional tests, so a team familiar with Drupal development can start with those. The risks are slow progress, inconsistent test design across developers, and a suite that nobody maintains after the first release. An external partner can be useful for the initial architecture, the CI setup and a code review of the first tests, after which the internal team carries the work. Decide early who owns the suite, because tests that no one owns become skipped or deleted when deadlines press. Ask any partner what will be handed over, in what form, and what the internal team needs to know to continue.

How do automated tests change across Drupal major upgrades?

Tests are one of the best protections against upgrade breakage, but they also need work during upgrades. Drupal 10 requires PHP 8.1 or later, Drupal 11 requires PHP 8.3 or later and Drupal 12 requires PHP 8.5 or later, so the PHP version in CI must move with the target release. Deprecated APIs flagged by PHPStan and by Drupal's deprecation output show which code needs changes before the upgrade. Functional and browser tests reveal theme and markup regressions that unit tests miss. Drupal 12 is scheduled for the week of December 7, 2026, and Drupal 10 reaches end of life on December 9, 2026, so teams on Drupal 10 should plan test updates in advance. Keep tests independent of implementation details where possible, so they survive refactoring.

What does ongoing maintenance of a Drupal test suite involve?

Maintenance is recurring work. Drupal core publishes bugfix releases on the first Wednesday of each month and security releases on the third Wednesday, so CI should run against each update to catch regressions early. Teams need to fix flaky browser tests, update fixtures when content models change, keep PHPStan baselines honest, and retire tests for features that were removed. Each new custom feature should come with its own tests, which means the suite grows with the codebase. Budget for this as a continuing responsibility with a named owner, not a project that ends at handover. Ask for the expected cadence of test reviews and how failing tests are triaged, and confirm whether ongoing support is handled through a retainer or by the internal team.

How testing fits into a Drupal upgrade plan

A test suite that covers your custom code is what makes a Drupal upgrade plan schedulable instead of open-ended. Drupal 11 is the current major version, Drupal 12 is scheduled for the week of December 7, 2026, and Drupal 10 reaches end of life on December 9, 2026, according to the Drupal core release schedule. Those dates give you a fixed window, and the work inside it is mostly finding out which of your custom modules, themes and services break when core, contributed projects or PHP change underneath them. Tests answer that question on a branch, before the upgrade is scheduled, rather than during a production cutover.

Deprecation checks and regression tests do different jobs and both belong in the plan. Static analysis with PHPStan, run at a level your team has agreed, flags calls to APIs that core has changed or removed. Kernel and functional tests in phpunit.xml show whether your services, forms, access rules and entity handling still behave the same after a core update. Ask your team to tag which tests cover custom code only, because a failing contributed-module test is a question for that project, while a failing test in your own custom directory is your code to fix. Keep the suite pointed at your modules and themes so the results stay attributable.

Bugfix releases arrive on the first Wednesday of each month and security releases on the third Wednesday, per the same release schedule. That cadence means a CI pipeline should run the suite against each new core and contributed release on those days, not only when someone remembers to update. Confirm with your delivery team that the pipeline can apply a core update on a branch, run the suite and report the result before anyone merges. A green run on those days is evidence that your custom code still works on the patched release, and a red run is the early warning that the next major upgrade will need more effort than planned.

How DeployOpen works on this

DeployOpen plans and builds Drupal test automation as part of a self-hosted Drupal project, inside your own infrastructure. We scope the test layers, the critical user journeys and the CI pipeline in writing before work starts, then hand over a suite your own developers can run and extend.

How we scope

  • Written scope lists which custom modules and themes get PHPUnit unit, kernel and functional tests.
  • Critical user journeys for browser tests are agreed with your business owners before any test is written.
  • Scope names the PHPStan level, coding standards checks and CI runner you will use, with the environment each test needs.

How we build

  • We write tests alongside custom code, starting with kernel tests for services, entities and access logic.
  • Browser tests cover the agreed user journeys and run against a production-like copy of your Drupal configuration.
  • Static analysis and code style checks run on every merge request so failures appear before code review starts.

How we hand over

  • You receive the test suite, CI configuration, a short guide to running tests locally and in your pipeline.
  • Documentation records which tests cover which custom code, so upgrades show what needs attention first.
  • Your developers run a walkthrough session on writing a new test and reading a failing CI report.

How we support

  • Engagements are fixed-scope, and the written scope sets the deliverables and timeline before work starts.
  • Ongoing test maintenance, upgrade checks and pipeline changes are available as a retainer.
  • Confirm with us how test fixtures, database snapshots and secrets for CI will be stored and rotated in your environment.

Talk to our team.

Tell us what you're working on, whether it's a deployment, an audit, a security test or a cyber range. You'll speak with an engineer who can help you scope it.

  • 30-minute call: free, with no obligation.
  • NDA on request: we can sign before you share details.
  • Clear next steps: a scope and plan after the call.