Testing a Drupal site with Cypress, the way I wish someone had shown me
I have been writing Drupal code on and off for about six years, and the part of the job I avoided the longest was automated testing. Not because I thought tests were a waste of time, but because the Drupal testing story used to be a maze. PHPUnit with the Drupal testing framework, Behat for behavior-driven scenarios, Simpletest for legacy code paths, and a configuration file that took longer to write than the test it was supposed to support. The first time I tried to add a Cypress (a browser-based testing tool that runs JavaScript in a real browser to simulate what a user sees and does) integration to a Drupal project, I gave up after two hours and shipped the feature without tests. The bug came back three weeks later, and the bug took longer to fix than writing the test would have.
I am writing this because if you are in the same boat, the path is shorter now than it was. Cypress has matured, the Drupal-Cypress community has produced a real integration layer, and you do not have to fight the framework to get something useful. Here is what I do today, what still annoys me about it, and the parts I would skip if I were starting over.
Why Cypress, and why not just keep using Drupal’s test framework
Drupal ships with a PHPUnit-based test framework that has been part of core for over a decade. If you have been working in Drupal for a while, you have used it: you extend a base test class, write a bunch of $this->drupalGet(...) calls, and pray that the database sandbox (a per-test copy of the database that Drupal creates and destroys around each test run, so tests do not pollute each other) actually resets between runs. For unit tests of business logic, it is fine. For anything that touches JavaScript, it is the wrong tool. The test runs in a stripped-down PHP environment that does not execute the JavaScript you actually want to verify, and you end up writing tests that confirm your code rendered an HTML element while completely missing the part where the JavaScript broke.
Cypress is the opposite. It opens a real browser, runs your real JavaScript, and lets you write tests as a sequence of “I click this, I type this, I expect this to appear.” For a Drupal site, that is the level at which most of the interesting bugs live. Form submissions that fail because the AJAX callback returns the wrong shape. JavaScript behaviors that quietly break after a content type is updated. Modal dialogs that lose their state when a user navigates back. None of these are testable with PHPUnit, and all of them are testable with Cypress.
Two things have changed since 2023 that make the integration less painful. First, the Drupal community has standardized on the drupal/cypress project, which handles Drupal-specific Cypress configuration out of the box: where the Drupal site lives, how to authenticate an admin user, how to clear caches between tests. Second, Cypress itself has gotten faster and more reliable. The 12.x and 13.x releases fixed a long-standing issue where the test browser would freeze on a long-running command, and the new component testing (a Cypress feature that lets you mount and test individual UI components in isolation, without spinning up the whole application) mode is genuinely useful if you have a design system to maintain.
A setup that actually works
The setup I use today assumes a Lando-based local environment (Lando is a tool that builds and manages local development containers based on Docker, so every developer on a team can run the same Drupal site without fighting PHP, MySQL, and dependency versions). The reason for that assumption is that I work on small teams where everyone has a slightly different laptop OS, and Lando is the only way I have found to guarantee that “works on my machine” actually means something. If you are using DDEV (another popular local-Drupal tool built on Docker) or a hand-rolled Docker setup, the same Cypress tests will work, but the file paths and environment variables will be different.
The structure of a Cypress-Drupal project looks like this:
tests/cypress/is where the test code lives. Inside that,e2e/is for full end-to-end tests (the full user journey through a real browser),component/is for component tests, andfixtures/holds test data like JSON blobs or image files.tests/cypress/support/holds custom Cypress commands. The most important one is usually a customcy.drupalLogin(username, password)that handles the Drupal login form, including the CSRF token (a unique, single-use token Drupal embeds in forms to prove the request came from a real page on your site, not a cross-site forgery).cypress.config.jsat the project root tells Cypress where the Drupal site is, where to find the support files, and what browser to use. With thedrupal/cypresspackage, much of this is preconfigured.package.jsonat the project root declares Cypress as a dev dependency. Runnpm installonce, thennpx cypress opento launch the test runner.
On the first setup, the configuration takes about an hour. The second time, it takes about fifteen minutes. The third time, you copy the cypress.config.js from a previous project and adjust two environment variables. That is the right ratio. If the setup is taking longer than that, you are probably trying to do too much in the initial configuration. Start with one end-to-end test, get it passing, and add more tests from there.
Tests that are actually worth writing
Not every test is worth writing. The most valuable Cypress tests for a Drupal site are the ones that catch regressions in the user-visible parts of the site: forms that take user input, content editing workflows, search and filtering, and anything that uses AJAX. A test that just confirms “the homepage loads and shows the site name” is a waste of time. A test that confirms “an editor can create a new article, upload an image, save it, and the article appears on the public listing page” is worth its weight in gold.
The pattern I have settled on is to write a small library of “user journey” tests that cover the most common paths through the site, plus a separate set of “smoke” tests that run against a fresh Drupal install to confirm the build itself is not broken. The user journey tests are the slow ones (each one might take ten or twenty seconds), and the smoke tests are the fast ones (under a second each). The user journey tests run before every release. The smoke tests run on every commit.
Component tests are the third layer, and they are the most controversial. If your Drupal site has a custom design system, component tests let you test individual UI elements in isolation. I find them useful for the parts of the site that are reused in many places (a date picker, a modal, a content card). I find them less useful for one-off page templates. A test for “the date picker accepts a date and emits a change event” is worth writing. A test for “this specific page template renders the title at the top and the body below” is not.
The other pattern that has paid off for me is data-driven testing. Cypress lets you parameterize tests over a list of inputs, which is great for forms with many fields. A single test that goes through a list of ten different field combinations will catch more bugs than ten separate tests with hardcoded data, and the test code stays shorter.
What I would tell past me
If I could send a message back to the version of me that gave up after two hours, I would say four things.
- The setup is the hard part, and only the first time. Once you have a working
cypress.config.jsand one passing test, every additional test is incremental. Do not try to set up the full test suite on day one. - The
drupal/cypresspackage is your friend. Most of the Drupal-specific plumbing you would write yourself (login, cache clearing, test database reset) is already in there. Use it. - Component testing is not always worth it. It is a tool, not a default. Reach for it when you have a real reusable component, not because the framework supports it.
- Run tests in CI, not on your laptop. Cypress tests on a developer laptop will pass locally and break in CI for reasons that are hard to debug. Use GitHub Actions, GitLab CI, or CircleCI, and pin the Cypress version. Local-only testing is a trap.
Trade-offs
The biggest honest cost of Cypress is that it is a JavaScript project running on top of a PHP project. You need Node.js installed, you need a working npm registry, and you need to keep the Cypress version aligned with the rest of the JavaScript toolchain in the project. If your team is PHP-only, this is real friction. The first week of using Cypress on a PHP-only team will be a lot of “why is this Node thing broken” and not a lot of testing.
A second cost is the test runtime. End-to-end tests in a real browser are slower than unit tests by an order of magnitude. A Drupal test suite with 30 user journey tests will take 5-10 minutes to run, even with parallelism. For a CI pipeline that deploys on every commit, that is a meaningful tax. Plan for it, and consider running the full Cypress suite only on pull requests, not on every push.
Maintenance is the third cost. Cypress tests break when the UI changes, and a Drupal admin UI change will sometimes break a dozen tests at once. The fix is usually small, but the volume adds up. Budget at least a few hours per month for test maintenance, and do not assume that “write once, run forever” is the Cypress experience. It is closer to “write once, fix forever.”
If you are working on a Drupal site with a small team, a stable UI, and a CI pipeline you already trust, Cypress is a clear win. If you are on a one-person team maintaining a site that rarely changes, the setup cost is not worth it. If you are running Drupal Commerce or a heavily customized Drupal 7 site, Cypress will work but the integration is rougher, and you should plan for more upfront work.