Cypress Tasks vs. Commands: What Are They and When to Use Them

While using Cypress, sometimes it gets confusing trying to understand when to use a task or custom command. It’s easy to misuse custom commands in your testing, which leads to unpredictable results in your end-to-end tests. Let me outline some of the key differences and usages of Cypress commands and tasks. This will leave you better equipped for writing successful Cypress end-to-end tests.

Cypress Commands

A Cypress custom command is useful for automating a workflow that repeats in your tests. Developers primarily use this for grouping several cy commands. Each command returns a Chainable type that allows you to further interact with your web app by making queries and assertions.

Commands are executed serially but are enqueued immediately in the same event loop tick. This is why you cannot assign values and use them later with other commands. They simply will not exist.

// Bad example
let value;
cy.get(#input-field).invoke('val').then((input) => {
  value = input;
});
cy.get(#another-input).type(value); // undefined
// Another bad example
let value;
cy.request({
  method: 'GET',
  url: `/api/username`,
}).then((result) => {
  value = result.body;
});
cy.get(#input-field).type(value); // undefined

Commands are also not promises. Until a previous command finishes, the next command doesn’t execute. Cypress guarantees to run all of its commands deterministically and identically every time they are run. Cypress serially runs these commands to create consistency and prevent flakey tests!

// Bad example
let value;
await cy.request({
  method: 'GET',
  url: `/api/username`,
}).then((result) => {
  value = result.body;
});
cy.get(#input-field).type(value); // undefined
// Another bad example
const value = await cy.get(#input-field).invoke('val');
cy.get(#input-field).type(value); // undefined

Commands always work best written serially as Cypress intended:

// Login with cy commands in test
cy.get(#username).type('username');
cy.get(#password).type('password');
cy.get('button').contains('Login').click();
cy.get('body').contains('Welcome Home');

You can also write custom commands for pre-defined workflows that might repeat in your tests. A good example is the previous login flow:

// Create a custom command
Cypress.Commands.add('login', (username, pw) => {
  cy.get(#username).type('username');
  cy.get(#password).type('password');
  cy.get('button').contains('Login').click();
  cy.get('body').contains('Welcome Home');
});

// Login with the command in your test
cy.login('username', 'password');

Cypress Tasks

In contrast, a Cypress task is a function defined and executed in Node. It allows your tests to “jump” from the Cypress browser process to the Cypress Node process. A task is enqueued like a regular command via cy.task. However, when the command takes its turn to execute, the backend process will run the code asynchronously. Data is then returned to the browser serialized, and you can access the value via a .then(callback).

Tasks are best used for functionality such as seeding a database, communicating with your backend, storing state in Node, and running external processes. If you are looking to run a promise in Cypress, tasks are what you want.

// Custom task in plugins/index.js
import axios from 'axios';

module.exports = (on, config) => {
  on('task', {
    getVerificationCode(args: { userId: string; password: string; }) {
      const result = await axios.request({
          method: 'GET',
          url: '/api/getVerification'
          params: {
            userId: args.userId
          },
          body: {
            password: args.password
          }
        });
      expect(result.status).to.eq(200); // Can make assertions too!
      return result.data;
    },
  });
}
// Using custom task in test code
cy.task('getVerificationCode', { userId, password }).then((value) => {
  // Can use the value in commands.
  cy.get('#verification-field').type(value);
});

Cypress Commands and Tasks

Hopefully, now you know the difference between Cypress commands and tasks. The key takeaways are:

  • If you need to run a promise or interact with your backend, go with a task. Remember: Commands are not promises.
  • If you are interacting with the DOM and making assertions, go with a command.
  • When a series of commands are repeated multiple times, create a custom command.