How to Use Cypress for Testing Drupal

Illustration of two people on a cliff overlooking a river.

If you’re not including tests in your Drupal development, chances are you think that it adds complexity and expense without providing enough benefit. Cypress is easy to learn and implement, will protect against regression as your projects become more complex and can make your development process more efficient.

Cypress is a tool for reliably testing anything that runs in a web browser. It’s open source and web platform agnostic (think: great for testing projects using front-end technologies like React) and highly extensible. It’s an increasingly popular tool and was the “#1 tool to adopt” according to Technology Radar in 2019.

In this blog, I’ll cover three topics to help you start testing in your Drupal project using Cypress:

  • Installing Cypress
  • Writing and running basic tests using Cypress
  • Customizing Cypress for Drupal

Installing Cypress

Let’s assume that you created your Drupal project using composer as recommended on Drupal.org:

$ composer create-project drupal/recommended-project cypress

Your project will have (at least) this basic structure:

vendor/
 
web/
 
.editorconfig
 
.gitattributes
 
composer.json
 
composer.lock

The cypress.io site has complete installation instructions for various environments; for our purposes we’ll install Cypress using npm.

Initialize your project using the command npm init. Answer a few questions that node.js asks you and you’ll have a package.json file that looks something like this:

{
 
  "name": "cypress",
 
  "version": "1.0.0",
 
  "description": "Installs Cypress in a test project.",
 
  "main": "index.js",
 
  "scripts": {
 
    "test": "echo \"Error: no test specified\" && exit 1"
 
  },
 
  "author": "",
 
  "license": "ISC"
 
}

Now you’re ready to install Cypress in your project:

npm install cypress --save-dev

Let’s run Cypress for the first time:

npx cypress open

Since we haven’t added any config or scaffolding files for Cypress, it will observe It looks like this is your first time using Cypress and open the Cypress app to the welcome screen, showing both E2E Testing and Component Testing as “Not Configured”. Let’s configure your project for E2E testing: click the “Not Configured” button for E2E Testing. Cypress will report that it added some files to your project:

cypress/
 
node_modules/
 
vendor/
 
web/
 
.editorconfig
 
.gitattributes
 
composer.json
 
composer.lock
 
cypress.config.js
 
package-lock.json
 
package.json

Click on “Continue”, then choose your preferred browser for testing and click “Start E2E Testing in [your browser of choice]”.

In a separate window, Chrome will open to the Create your first spec page:

Click on the ‘Scaffold example specs’ button and Cypress will create a couple of folders with example specs to help you understand a bit more about how to use Cypress. Read through them in your code editor and you’ll find that the language (based in js) is intuitive and easy to follow; click on any in the Chrome test browser and you’ll see two panels: a text panel on the left, showing each step in the active spec and a simulated browser window on the right, showing the actual user experience as Cypress steps through the spec.

Open the cypress.config.js file in your project root and change it as follows:

const { defineConfig } = require("cypress");
 
 
module.exports = defineConfig({
 
  component: {
 
    fixturesFolder: "cypress/fixtures",
 
    integrationFolder: "cypress/integration",
 
    pluginsFile: "cypress/plugins/index.js",
 
    screenshotsFolder: "cypress/screenshots",
 
    supportFile: "cypress/support/e2e.js",
 
    videosFolder: "cypress/videos",
 
    viewportWidth: 1440,
 
    viewportHeight: 900,
 
  },
 
  e2e: {
 
    setupNodeEvents(on, config) {
 
      // implement node event listeners here
 
    },
 
    baseUrl: "https://[your-local-dev-url]",
 
    specPattern: "cypress/**/*.{js,jsx,ts,tsx}",
 
    supportFile: "cypress/support/e2e.js",
 
    fixturesFolder: "cypress/fixtures"
 
  },
 
});

Change the baseUrl to your project’s url in your local dev environment.

These changes tell Cypress where to find its resources and how to find all of the specs in your project.

Writing and running basic tests using Cypress

Create a new directory called integration in your /cypress directory, and within the integration directory create a file called test.cy.js.

cypress/
 
├─ e2e/
 
├─ fixtures/
 
├─ integration/
 
│  ├─ test.cy.js
 
├─ support/
 
node_modules/
 
vendor/
 
web/
 
.editorconfig
 
.gitattributes
 
composer.json
 
composer.lock
 
cypress.config.js
 
package-lock.json
 
package.json

Add the following contents to your test.cy.js file:

describe('Loads the front page', () => {
 
  it('Loads the front page', () => {
 
    cy.visit('/')
 
    cy.get('h1.page-title')
 
      .should('exist')
 
  });
 
});
 
 
describe('Tests logging in using an incorrect password', () => {
 
  it('Fails authentication using incorrect login credentials', () => {
 
    cy.visit('/user/login')
 
    cy.get('#edit-name')
 
      .type('Sir Lancelot of Camelot')
 
    cy.get('#edit-pass')
 
      .type('tacos')
 
    cy.get('input#edit-submit')
 
      .contains('Log in')
 
      .click()
 
    cy.contains('Unrecognized username or password.')
 
  });
 
});

In the Cypress application when you click on test.cy.js you’ll watch each of the test descriptions on the left while Cypress performs the steps in each describe() section.

This spec demonstrates how to tell Cypress to navigate your website, access html elements by id, enter content into input elements and submit the form. (I discovered that I needed to add the assertion that the <input id="edit-submit"> element contains the text ‘Log in’ before the input was clickable. Apparently the flex styling of the submit input was impeding Cypress’ ability to “see” the input, so it couldn’t click on it.)

Customizing Cypress for Drupal

You can write your own custom Cypress commands, too. Remember the supportFile entry in our cypress.config.js file? It points to a file that Cypress added for us which in turn imports the './commands' file(s). (Incidentally, Cypress is so clever that when importing logic or data fixtures you don’t need to specify the file extension: import './commands', not './commands.js'. Cypress looks for any of a dozen or so popular file extensions and understands how to recognize and parse each of them.)

But I digress.

Define your custom commands in this commands.js file, such as:

/**
 
 * Logs out the user.
 
 */
 
Cypress.Commands.add('drupalLogout', () => {
 
  cy.visit('/user/logout');
 
})
 
 
/**
 
 * Basic user login command. Requires valid username and password.
 
 *
 
 * @param {string} username
 
 *   The username with which to log in.
 
 * @param {string} password
 
 *   The password for the user's account.
 
 */
 
Cypress.Commands.add('loginAs', (username, password) => {
 
  cy.drupalLogout();
 
 
  cy.visit('/user/login');
 
  cy.get('#edit-name')
 
    .type(username);
 
  cy.get('#edit-pass').type(password, {
 
    log: false,
 
  });
 
  cy.get('#edit-submit').contains('Log in').click();
 
});

Here we’re defining a custom Cypress command called drupalLogout(), which we can use in any subsequent logic – even other custom commands. To log a user out, call cy.drupalLogout(). This is the first thing we’re doing in our custom command loginAs, in which the first thing we do is ensure that Cypress is logged out before attempting to log in as the user specified by the username and password parameters.

Using environment variables you can even define a Cypress command called drush() – using some helper functions – with which you can execute drush commands in your test writing or custom commands. How simple is this for defining a custom Cypress command that logs a user in using their uid?

/**
 
* Logs a user in by their uid via drush uli.
 
*/
 
Cypress.Commands.add('loginUserByUid', (uid) => {
 
 cy.drush('user-login', [], { uid, uri: Cypress.env('baseUrl') })
 
   .its('stdout')
 
   .then(function (url) {
 
     cy.visit(url);
 
   });
 
});

This uses the drush user-login command (drush uli for short) and takes the authenticated user to the site’s base url.

Consider the security benefit of never having to read or store user passwords in your testing. Personally I find it amazing that a front-end technology like Cypress can execute drush commands, which I have always thought of as being very much on the back end.

You’ll want to familiarize yourself with Cypress fixtures as well (fixtures are files that hold test data) - but that’s a little outside the scope of this post. Please see Aten’s webinar Cypress Testing for Drupal Websites, particularly the section on fixtures that begins at 18:33. That webinar goes into greater detail about some interesting use cases – including an ajax-enabled form - and includes a scene from Monty Python and the Holy Grail which I hope you enjoy.

Because our VP of Development, Joel Steidl, helped me define drush() as a custom Cypress command, I’d like to share that with you as well. Please feel free to use or fork Aten’s public repository on Cypress Testing for Drupal.

Happy testing!

Drupal Automated Testing

Read This Next