DEV Community

Cover image for Level Up Your Cypress Game: Cypress Smart Tests Plugin
S Chathuranga Jayasinghe
S Chathuranga Jayasinghe

Posted on

Level Up Your Cypress Game: Cypress Smart Tests Plugin

A smart solution for dependant tests, conditional test execution on runtime and individual test-bound hooks and many more..

In the realm of test automation, ensuring that end-to-end tests are both efficient and maintainable is a continuous challenge. While Cypress has revolutionized the way we approach testing, there are scenarios where we wish our tests could be more… SMART! 

Introducing the Cypress Smart Tests plugin

The plugin cypress-smart-tests is designed to elevate your test execution by introducing advanced control mechanisms directly into your Cypress tests. This plugin will solve common pain points in E2E test orchestration by:

  • Letting you define dependencies between tests
  • Running tests conditionally based on runtime logic
  • Enabling before/after hooks per test block

Okie dokie! Let’s explore how to install, configure, and use it effectively..

Setup and Configure

First things first, initiate a project and install Cypress. It does not matter whether it is JavaScript or TypeScript. Then install the plugin as a dev dependency: 

npm install — save-dev cypress-smart-tests

Whenever you are writing a Spec file, import the helpers to your file:

import { cytest, defineTestDependencies, configure, cyVariables } from 'cypress-smart-tests';
Enter fullscreen mode Exit fullscreen mode

Now, instead of using the usual it() block, use cytest() — a drop-in replacement with smart capabilities ❤


Features

Conditional Test Execution

If you’ve been in the test automation realm for sometime, you already know that you will come across lots of scenarios where you have to execute your test based on various conditions. Either based on environment variables/feature or runtime conditions. Some of these scenarios could be:

  • test only on mobile viewports
  • run only if a flag is enabled

It doesn’t matter whatever the condition is, you can define your condition with the cytest() block and execute your test based on the conditions met. Check this out:

Image description

Image description

Problem solved! 😉 Let’s move on to the next pain-point..


Dependant Tests

Well well well.. I know! The “Best practice is to avoid dependant tests!”, BUT we all know that sometimes — at least rarely we get to situation where we have to write dependant tests. So, this plugin is gonna solve that problem too. 

First you have to define your test dependencies like this:

Image description

Hold on! Let’s consider kind of a complex scenario. We have 6 tests in our Spec file: 

  • Test2 & Test3 depends on Test 1
  • Test5 depends only on Test4
  • Test6 is an independant test

Still you can achieve this with the plugin like this:

Image description

When test execution happens, it behaves this way:

  • If Test1 fails: Execution of the tests Test2 & Test3 will be skipped! 
  • If Test4 fails: Execution of the test Test5 will be skipped!
  • Test6 will be executed no matter what happens with the other tests

What do we achieve through this? You might be thinking so WHAT? This makes a big difference, if you have dependant tests like this and you execute the tests in the traditional way, you will waste a lot of time! WHY? Because when the parent test fails, the dependant tests will obviously fail because the pre-condition of those are not met! Yet the dependant tests execute (with all the Cypress retries and default waiting times) and waste the time. Time is Gold!

So it’s important to fail-fast or skip-fast ❤ Here is a silly sample code with dependencies and tests:

import { cytest, defineTestDependencies, configure, resetState } from 'cypress-smart-tests';

describe('Cypress Smart Tests Plugin - Dependencies', () => {
    beforeEach(() => {
        resetState();
    });

    context('Simple Dependent Test Execution', () => {
        beforeEach(() => {
            // Configure failFast mode
            configure({ failFast: true });

            // Define dependencies
            defineTestDependencies({
                'Critical Test': ['Subsequent Test 1', 'Subsequent Test 2'],
            });
        });

        cytest('Critical Test', () => {
            // This test will fail
            cy.wrap(false).should('be.true');
        });

        cytest('Subsequent Test 1', () => {
            cy.log('This test should be skipped in failFast mode');
            cy.wrap(true).should('be.true');
        });

        cytest('Subsequent Test 2', () => {
            cy.log('This test should also be skipped in failFast mode');
            cy.wrap(true).should('be.true');
        });

        cytest('Subsequent Test 3', () => {
            cy.log('This test should not be skipped in failFast mode');
            cy.wrap(true).should('be.true');
        });
    });

    context('Complex Dependent Test Execution', () => {
        beforeEach(() => {
            // Define dependencies
            defineTestDependencies({
                'Critical Test': ['Subsequent Test 1', 'Subsequent Test 2'],
                'Other Critical Test': ['Subsequent Test 4'],
            });
        });

        cytest('Critical Test', () => {
            // This test will fail
            cy.wrap(false).should('be.true');
        });

        cytest('Subsequent Test 1', () => {
            cy.log('This test should be skipped in failFast mode');
            cy.wrap(true).should('be.true');
        });

        cytest('Subsequent Test 2', () => {
            cy.log('This test should also be skipped in failFast mode');
            cy.wrap(true).should('be.true');
        });

        cytest('Subsequent Test 3', () => {
            cy.log('This test should not be skipped in failFast mode');
            cy.wrap(true).should('be.true');
        });

        cytest('Other Critical Test', () => {
            // This test will fail
            cy.wrap(false).should('be.true');
        });

        cytest('Subsequent Test 4', () => {
            cy.log('This test should be skipped in failFast mode');
            cy.wrap(true).should('be.true');
        });
        cytest('Subsequent Test 6', () => {
            cy.log('This test should not be skipped in failFast mode');
            cy.wrap(true).should('be.true');
        });
    });
});

Enter fullscreen mode Exit fullscreen mode

Individual Test Hooks

While global hooks are a staple in testing frameworks including Cypress, there are instances where individual tests require specific setup or cleanup operations. The Smart Tests Plugin allows you to define custom hooks for individual tests while having the global hooks! Yes, you read it right!

I’ll take a simple scenario and provide you the code for it. Let’s assume If you’re seeding a test DB for a specific test case:

Image description


Persistent Variables

Speaking of variables, it’s a very common scenario we want to share variable values between tests. But by default they reset after the execution of the it() block. The cypress-smart-tests plugin provides you with a solution to define and persist variables across tests (not across Spec files). You can store and retrieve test variables across cytest() blocks like this:

Image description

Here is a full code snippet where variables are defined and used across tests:

import { cytest, cyVariable, cyVariables, resetState } from 'cypress-smart-tests';

describe('Cypress Smart Tests Plugin - Persistent Variables', () => {
    beforeEach(() => {
        resetState();
    });

    context('Basic Variable Usage', () => {
        // Set up a variable before tests
        before(() => {
            cyVariable('testVar', 'initial value');
        });

        cytest('Set and get a variable', () => {
            // Set a variable
            cyVariable('username', 'testuser');

            // Get the variable
            const username = cyVariable('username');

            // Verify the variable was set correctly
            expect(username).to.equal('testuser');
        });

        cytest('Variable persists across tests', () => {
            // Get the variable set in the previous test
            const username = cyVariable('username');

            // Verify the variable still has the same value
            expect(username).to.equal('testuser');

            // Update the variable
            cyVariable('username', 'updateduser');

            // Verify the update worked
            expect(cyVariable('username')).to.equal('updateduser');
        });

        cytest('Variables can store different types', () => {
            // Store a number
            cyVariable('count', 42);
            expect(cyVariable('count')).to.equal(42);

            // Store an object
            const user = { id: 1, name: 'Test User', active: true };
            cyVariable('user', user);
            expect(cyVariable('user')).to.deep.equal(user);

            // Store an array
            const items = ['item1', 'item2', 'item3'];
            cyVariable('items', items);
            expect(cyVariable('items')).to.deep.equal(items);

            // Store a boolean
            cyVariable('isActive', true);
            expect(cyVariable('isActive')).to.be.true;
        });

        cytest('Variable set in before hook is available', () => {
            // Get the variable set in the before hook
            const testVar = cyVariable('testVar');

            // Verify the variable has the expected value
            expect(testVar).to.equal('initial value');
        });
    });

    context('Multiple Variables Management', () => {
        before(() => {
            // Reset variables to ensure a clean state
            resetState(true);
        });

        cytest('Add and get variables', () => {
            // Add variables
            cyVariables().add('username', 'testuser');
            cyVariables().add('userId', 123);
            cyVariables().add('userPreferences', { theme: 'dark', language: 'en' });

            // Get variables
            const username = cyVariables().get('username');
            const userId = cyVariables().get('userId');
            const userPreferences = cyVariables().get('userPreferences');

            // Verify variables were set correctly
            expect(username).to.equal('testuser');
            expect(userId).to.equal(123);
            expect(userPreferences).to.deep.equal({ theme: 'dark', language: 'en' });
        });

        cytest('Check if variables exist', () => {
            // Check existing variables
            expect(cyVariables().has('username')).to.be.true;
            expect(cyVariables().has('userId')).to.be.true;
            expect(cyVariables().has('userPreferences')).to.be.true;

            // Check non-existing variable
            expect(cyVariables().has('nonExistingVar')).to.be.false;
        });

        cytest('Get all variables', () => {
            // Get all variables
            const allVariables = cyVariables().getAll();

            // Verify all variables are returned
            expect(allVariables).to.deep.equal({
                username: 'testuser',
                userId: 123,
                userPreferences: { theme: 'dark', language: 'en' }
            });
        });

        cytest('Remove a variable', () => {
            // Remove a variable
            cyVariables().remove('username');

            // Verify the variable was removed
            expect(cyVariables().has('username')).to.be.false;

            // Other variables should still exist
            expect(cyVariables().has('userId')).to.be.true;
            expect(cyVariables().has('userPreferences')).to.be.true;
        });

        cytest('Clear all variables', () => {
            // Clear all variables
            cyVariables().clear();

            // Verify all variables were cleared
            expect(cyVariables().getAll()).to.deep.equal({});
            expect(cyVariables().has('userId')).to.be.false;
            expect(cyVariables().has('userPreferences')).to.be.false;
        });
    });

    context('Variables with resetState', () => {
        before(() => {
            // Set up some variables
            cyVariable('testVar1', 'value1');
            cyVariable('testVar2', 'value2');
        });

        cytest('Variables persist after normal resetState', () => {
            // Reset state without resetting variables
            resetState();

            // Variables should still exist
            expect(cyVariable('testVar1')).to.equal('value1');
            expect(cyVariable('testVar2')).to.equal('value2');
        });

        cytest('Variables are cleared with resetState(true)', () => {
            // Reset state and variables
            resetState(true);

            // Variables should be cleared
            expect(cyVariable('testVar1')).to.be.undefined;
            expect(cyVariable('testVar2')).to.be.undefined;
        });
    });

    context('Practical Examples', () => {
        before(() => {
            // Reset variables to ensure a clean state
            resetState(true);
        });

        cytest('Store user credentials for reuse', () => {
            // Store user credentials
            cyVariables().add('credentials', {
                username: 'testuser',
                password: 'password123'
            });

            // Verify credentials were stored
            const credentials = cyVariables().get('credentials');
            expect(credentials.username).to.equal('testuser');
            expect(credentials.password).to.equal('password123');

            // Simulate login
            cy.log(`Logging in with username: ${credentials.username}`);
        });

        cytest('Use stored credentials in another test', () => {
            // Get credentials from previous test
            const credentials = cyVariables().get('credentials');

            // Verify credentials are still available
            expect(credentials).to.not.be.undefined;
            expect(credentials.username).to.equal('testuser');

            // Simulate using credentials
            cy.log(`Using stored credentials for user: ${credentials.username}`);
        });

        cytest('Store and update test data', () => {
            // Store initial test data
            cyVariable('testData', { id: 1, status: 'pending' });

            // Simulate updating the data
            const testData = cyVariable('testData');
            testData.status = 'completed';
            cyVariable('testData', testData);

            // Verify the update
            expect(cyVariable('testData').status).to.equal('completed');
        });
    });
});
Enter fullscreen mode Exit fullscreen mode

Keep in mind, that the variables are not persistent across Spec files. If you want variables that persist across Spec files, I highly recommend using the plugin ‘cypress-plugin-store’ by Lasitha Wijenayake!


Final Thoughts

The cypress-smart-testsplugin is a simple plugin I developed in my free-time ❤ This empowers you to write more smart and efficient tests by introducing mechanisms for defining dependencies, conditional execution, and custom hooks. By integrating this plugin into your Cypress test suite, you can enhance the maintainability and effectiveness of your end-to-end tests, ensuring that they remain robust and relevant as your application evolves.

Here is a full-demo code: https://github.com/s-chathuranga-j/cypress-smart-tests-demo-code

Let me know how it works for you and feel free to contribute or suggest improvements!

GitHub repo: https://github.com/s-chathuranga-j/cypress-smart-tests

Top comments (0)