0

Programmatic Conditional Test Logic for Cypress and Mocha

I've both asked a few different flavors of the above question and come across many different answers to this topic on StackOverflow so I've decided to do a broad stroke Q&A post that outlines a few different options to help tame this unruly beast.

Fair warning: the Cypress team hark on about conditional test execution being an anti-pattern. I think this is true in the vast majority of cases but there are still legitimate uses for conditional test execution (even if it's just debugging) and quite frankly, I'm more concerned with saving us all some time rather than making assumptions about the validity of your use case. So here goes.

The Nature of the Beast

Grouping tests and running them based on specific tags is, in essence, just another way of phrasing the question "how can I $(someAction) a test based on $(someCondition)?". If we zoom out a bit what all of these questions really boil down to is having a means to conditionally execute, skip, or select tests in the context of the Cypress or Mocha test frameworks.

The other common theme for many (but not all) of the posts on this topic is the desire to perform these checks at runtime, that is, the desire to programmatically choose what tests should be skipped/run/failed/etc.

This article is for people wanting to achieve their desired result programmatically, that is, by checking an expression that evaluates to a boolean value. For JavaScript that can mean a lot of things, read up on equality in JavaScript here.

Existing Plugins and Extensions

At the time of writing there are some pretty good Cypress plugins and npm packages that deal with this theme. If you're looking for something a bit more robust and don't mind adding some dependencies to your project for the sake of conditional test logic check these out:

  1. The skip-test npm package
  2. The cypress-tags npm package*
  3. The cypress-if plugin

The answers in the wiki are all DIY, for those wanting to know how to do this with just Cypress, Mocha, and (implied) Node.

GrayedFox
  • 2,350
  • 26
  • 44

1 Answers1

0

How do I fail a test?

Failing a test in Cypress and Mocha can be done in a myriad of ways but I am assuming you want to fail the test and log something meaningful as opposed to, say, cause a stack overflow error with an infinite while loop.

The easiest and most straight forward way to fail a test in Cypress is to do one of the following from your inside a Mocha hook, suite, or test function (a Mocha it, context, describe, before, beforeAll, after, or afterAll block):

  • assert(false, 'We failed our test!')
  • throw new Error('We failed our test!)

How do I skip a test?

Skipping a test can also be done in a myriad of ways. One non-programmatic way of doing this is to simply prepend the letter x to any Mocha test or test suite function. Thus:

  • it(...) becomes xit(...)
  • context(...) becomes xcontext(...)
  • describe(...) becomes xdescribe(...)

This works because these are the internal names that Mocha uses to mark a test or test suite as pending.

The problem with this approach is it requires the tests be renamed when what most people want to do is skip a test or test function at runtime based on some condition.

How do I skip a test programmatically?

Luckily for us Mocha ships with the ability to skip a test or test suite programmatically out of the box. The inclusive tests section of their docs delves deeper but basically, the rule of thumb is, it's better to skip tests programmatically than to comment them out since they will appear inside the test runner log as skipped tests, making it obvious that a test has indeed been skipped. The magic command? Simply call:

  • it.skip()
  • context.skip()
  • describe.skip()

Or

  • it.only()
  • context.only(...)
  • describe.only(...)

From within any Mocha hook or Mocha suite function. Knowing what we now know we can create a Cypress custom command that helps us achieve this task.

How can I skip a test using a custom command?

We're going absolute bare bones here and using an example command that is completely agnostic about the condition being checked. Leveraging some of JavaScript's features/design quirks (such as block scoped functions and the somewhat tricky Function.prototype.bind() method) we can make a custom command that can be called from anywhere inside your test suite, provided it has the right context (context as in this context):

// commands.ts
declare global {
  // eslint-disable-next-line @typescript-eslint/no-namespace
  namespace Cypress {
    interface Chainable {
      /**
       * Custom command which will skip a test or context based on a boolean expression.
       *
       * You can call this command from anywhere, just make sure to pass in the the it, describe, or context block you wish to skip.
       *
       * @example cy.skipIf(yourCondition, this);
       */
      skipIf(expression: boolean, context: Mocha.Context): void;
    }
  }
}

Cypress.Commands.add(
  'skipIf',
  (expression: boolean, context: Mocha.Context) => {
    if (expression) context.skip.bind(context)();
  }
);

The above command (using TypeScript) expects an expression that evaluates to a boolean (truthy) value which, if it does, will skip the given test or test suite. It does this by binding the scope of the given Mocha context to the original this context of the test block and immediately calling the newly bound function (note the extra () on the end of the method - that's intentional).

How do I skip an individual test?

Now you can skip, fail, or select an individual test for execution based on whatever condition it is you are evaluating:

describe('Events', () => {
  const url = `https://stackoverflow.com/question/ask`;

  context('Nested context', () => {
    // only this first test will be skipped
    it('test', function () {
      cy.skipIf(url.includes('/question/'), this);
      expect(this.stackOverflow.wiki).to.be('useful');
    });
    // this test will run
    it('test two', function () {
      expect(this.stackOverflow.answer).to.be('accepted');
    });
  });
});

Again, the custom command in our case skips the test but you can put what we've learned to good use here and do whatever you like instead. If you're really feeling fancy you could even pass in a function as a third parameter which is conditionally executed, or not.

How do I skip a test suite from within a before hook?

Using our custom command, the same way you skip a test. Note that this will skip all of the tests within the Mocha context, so:

describe('Events', () => {
  const url = `https://stackoverflow.com/question/ask`;

  before(function () {
    cy.visit(url);
    cy.skipIf(cy.url().includes('/flow.com/'), this));
  });
  // all of the below tests will be skipped
  context('Nested context', () => {
    it('test', function () {
      expect(this.stackOverflow.wiki).to.be('useful');
    });

    it('test two', function () {
      expect(this.stackOverflow.answer).to.be('accepted');
    });
  });
});

How do I group tests and skip them based on some condition?

Similar to how we achieve skipping a single test or test suite, we can also skip individual tests from different test suites based on a shared property. In this example we are going to check the test title inside a global beforeEach hook - this could be refactored into a custom command - it's just a minimal example meant as a proof of concept.

From inside your support file:

// i.e. e2e.ts
import './commands';

beforeEach(function () {
  const testTitle = Cypress.currentTest.title;
  if (testTitle.includes('unstable')) {
    this.skip();
  }
});

And from inside your specs:

describe('Events', () => {
  context('Nested context', () => {
    it('[api] test', function () {
      expect(this.stackOverflow.wiki).to.be('useful');
    });

    // this test will be skipped
    it('[api, unstable] test two', function () {
      expect(this.stackOverflow.answer).to.be('accepted');
    });
  });
});

describe('More Events', () => {
  context('Nested context', () => {
    // this test will be skipped
    it('[unstable] test', function () {
      expect(this.stackOverflow.wiki).to.be('useful');
    });

    it('test two', function () {
      expect(this.stackOverflow.answer).to.be('accepted');
    });
  });
});

What about running a single test?

Instead of using it.skip() you could create a custom command that instead runs it.only() and filter out tests that way. The world is your oyster.

How do I skip a test from somewhere else?

As long as you know what test or test suite you want to skip you could technically call the Cypress custom command (or just a generic helper method imported into your test file that has access to the Mocha module) from anywhere, so long as you have the right this context (which is the desired Mocha context). The thing is, because Cypress queues commands internally (and Mocha runs test hooks in a specific order too), you will also need to ensure that you don't attempt to skip a test before the Mocha context is known (before your variable is actually assigned the context).

That and Cypress will complain if you attempt to run a custom command outside of a test -- well -- it will complain if you attempt to run any cy.command() outside of a test, but that doesn't prevent you from using the Mocha module directly.

What this means is that skipping, failing, or selecting a test based on some logic is still inherently bound by the event order and events enqueued to be run by the Cypress or cy event emitters. You can deep dive into the Node event emitter API here but that stuff is a bit out of scope of this wiki so unfortunately, all you get is a link to the docs for this one.

I hope this has been useful to all you budding automation padowans out there - you got this!

GrayedFox
  • 2,350
  • 26
  • 44