Cumulocity App and Plugin Testing with Cypress - Getting Started

Why App and Plugin Testing?

Everyone can agree that having bugs in your Cumulocity application or Cumulocity Plugins is annoying. But do you know what’s even more annoying? It’s when your customers or users of your Cumulocity application are the ones who discover these bugs, not you. This is a scenario every developer aims to avoid. That’s one key reason why you should thoroughly test your Cumulocity application and plugins.

Following on from the importance of catching bugs early, end-to-end (E2E) tests play a pivotal role in the overall testing strategy. E2E tests are designed to simulate real-world usage scenarios of your Cumulocity application and its plugins, ensuring that the entire system functions as intended from start to finish. While unit tests scrutinize individual components in isolation, E2E tests evaluate the complete flow and user experience across the entire application under various conditions. This form of testing is crucial for identifying issues that might not emerge during unit or integration testing, such as problems with the application’s workflow, user interface glitches, or issues related to the application’s interaction with external systems and networks. By conducting E2E tests, you ensure that your application delivers the intended experience to users, thereby enhancing the reliability and usability of your product.

Moreover, E2E testing can safeguard against the embarrassment and potential loss of trust that may occur when customers encounter bugs before you do. It allows you to proactively address issues, enhancing the quality of your application before it reaches the end user. In essence, E2E testing is not merely about detecting bugs; it’s about ensuring that your application is user-friendly, performs well under real-world conditions, and meets your users’ needs without causing frustration or inconvenience. There’s nothing worse than a disappointed or frustrated customer.

computer-throw
A frustrated customer :frowning:

In this article, we will walk you through the process of setting up e2e tests for your Cumulocity application utilizing the Cypress framework, in conjunction with the Cumulocity Cypress package. This specialized package augments Cypress by providing a collection of predefined commands specifically designed for the Cumulocity environment, dramatically reducing the need for repetitive boilerplate code.

Prerequisite: This article is designed for readers who already possess a foundational understanding of Cypress and Cumulocity web development. If you’re looking to broaden your knowledge on Cumulocity web development or are just starting out, we highly recommend exploring the Cumulocity Web Development Tutorial Series.

Setup Cypress and the Cumulocity Cypress Package

Clone the Favorites Manager Plugin

First, we’ll begin by configuring the Cumulocity project that we aim to test using Cypress. For our example, we’ll focus on the Favorites Manager Plugin, which will serve as our case study for writing integration tests. The Favorites Manager Plugin allows you to mark any device, group or Digital Twin Manager asset as your favorite. These favorites can then quickly be accessed in your personal favorites list.

favorites-manager-01

We want to write two tests for the Favorites Manager Plugin.

  1. Validate if the favorites list exists and can be accessed via the navigation menu.
  2. Test if a random asset can be added to the personal favorites list and can be removed again afterwards.

If you want to follow along and implement the tests yourself, you can do so by cloning the Favorites Manager Plugin repository and checkout the branch cypress-tutorial:

git clone -b cypress-tutorial https://github.com/SoftwareAG/cumulocity-favorites-manager-plugin.git

Hint: Don’t forget to run npm install once you have cloned the project. Replace the placeholder {{C8Y_URL}} in the package.json, if you want to run the project locally.

The final code and all tests are available on the main branch in the repository.

Install Cypress

Next, let’s install Cypress. Cypress is an all-in-one testing framework designed for modern web applications, providing developers with tools to write, run, and debug tests directly in the browser. Install the latest Cypress version by running following command:

npm install cypress --save-dev

After Cypress has been installed, create two new files. The first file must be named cypress.config.ts and has to be created in the root directory of your project. The cypress.config.ts file in the Cypress framework serves as the central configuration file for your Cypress project. It allows you to define and customize various settings and options for how Cypress runs your tests.

Add following content to your cypress.config.ts:

import { defineConfig } from 'cypress';

export default defineConfig({
  e2e: {
    baseUrl: 'http://localhost:9000/',
  },
  viewportWidth: 1920,
  viewportHeight: 1080,
});

By setting baseUrl to http://localhost:9000/, we configure Cypress to execute all tests against this particular Url.

Inside your root directory create a new folder called cypress and in this folder create another folder named support. In the support directory create a new file, which is called e2e.ts. We’ll leave the file blank for now.

To finish setting up Cypress, add a new script to the scripts section of your package.json file:

"cypress:open": "cypress open"

If you start your application locally, in case of the Favorites Manager Plugin we will run the Plugin inside the Cockpit shell by executing the command npm run start (Make sure the placeholder {{C8Y_URL}} is replaced with an actual Cumulocity Url), the local server listens on port 9000. After starting your application, you can start Cypress by running npm run cypress:open. It will connect to http://localhost:9000/ as previously configured. Select E2E Testing in the Welcome screen and choose a browser, in which you want to run your Cypress tests. Finally, you should see this screen:

As we haven’t defined any tests yet, there is nothing for Cypress to test.

Install Cumulocity Cypress Package

Normally, the first steps in implementing your Cypress tests would involve a routine process:

  1. Creating a temporary user with specific roles for testing purposes, which should be deleted after the tests are completed.
  2. Navigating to a specific Cumulocity URL using cy.visit.
  3. Engaging with the login dialog to input credentials for the newly created user.

These repetitive tasks can lead to a significant amount of redundant code, because you will have to implement the integration to Cumulocity by yourself, e.g. for creating/deleting users or logging into the application via the login screen. In response to this challenge, we at Cumulocity have developed the Cumulocity Cypress package. This package provides a collection of Cypress commands designed to reduce boilerplate code and expedite the development of Cypress tests within the Cumulocity ecosystem.

Let’s install the Cumulocity Cypress package by executing following command:

npm install cumulocity-cypress --save-dev

If you install the cumulocity-cypress package in one of your projects, make sure the project has the peer-dependencies already installed. In particular, check if you have @angular/common, @c8y/client and cypress installed:

"peerDependencies": {
  "@angular/common": ">=14.0.0",
  "@c8y/client": ">=1015.0.0",
  "cypress": ">=12.0.0"
}

Once the package has been installed, create a new file in the root directory of your project called cypress.env.json. This file is used configure the Cumulocity Cypress package. In our case we will use the file to provide information about the tenant and the technical user, which should be used for the Cypress tests.

In your cypress.env.json configure following environment variables:

{
  "C8Y_TENANT": "t1234567",
  "C8Y_USERNAME": "christian.guether@softwareag.com",
  "C8Y_PASSWORD": "****"
}

Important The Tenant ID and credentials configured in the cypress.env.json must match and be valid for the Cumulocity URL you have configured for your Cumulocity application/plugin in the package.json

Finally, you must import the commands provided by the Cumulocity Cypress package into your Cypress environment. Update the content of the ./cypress/support/e2e.ts file and add following import:

import "cumulocity-cypress/lib/commands/";

This concludes the setup and configuration of your Cypress environment. Next, let’s have a look how a first simple test can be implemented.

First Cypress Tests

Check if favorites list exists

In the first test we want to keep it simple and check if the Favorites Manager Plugin is correctly registered in Cumulocity’s menu. If the menu item exists, we click on it and validate if the correct component is loaded and displayed to the user. For the test we don’t want to use a technical user, but instead create a new temporary user, who will be used for the test and will be deleted again after the test finished.

Create a new folder inside the cypress directory and name it e2e. That’s going to be the space in which we will define all of our tests. Inside the e2e folder create a new file named favorites-manager.cy.ts. Add following snippet to the favorites-manager.cy.ts:

import { IUser } from '@c8y/client';

describe('Favorites Manager', () => {
  // define a user, which should be used for the test instead
  // of technical users, which are only used for the test setup
  const testUser = {
    userName: 'testuser',
    password: 'ZVfJbDXuN!3t',
    displayName: 'Test User',
    email: 'test.user@softwareag.com',
  } as IUser;

  // create a new user before the test suite runs, who has the necessary roles
  // and permissions to access the Cockpit application extended with the Favorites Manager module
  // create a new user before the test suite runs, who has the necessary roles
  // and permissions to access the Cockpit application extended with the Favorites Manager module
  before(() => {
    Cypress.session.clearAllSavedSessions();

    cy.getAuth().login();
    cy.createUser(testUser, ['business'], ['cockpit']);
  });

  // login with the new user before each test
  beforeEach(() => {
    cy.getAuth(testUser.userName, testUser.password).login();

    cy.hideCookieBanner();
  });

  // delete the user after the test suite runs
  after(() => {
    cy.getAuth().login().deleteUser(testUser);
  });

  it('should load favorites list when clicking on favorites menu item', () => {
    // open the Cockpit application extended with the Favorites Manager module locally
    // and wait for the navigator menu to be visible
    cy.visitAndWaitForSelector(
      '/apps/cockpit/index.html?remotes=%7B"sag-ps-iot-pkg-favorites-manager-plugin"%3A%5B"FavoritesManagerModule"%5D%7D#',
      'en',
      '#navigator'
    );

    // check for the favorites menu item and click on it
    cy.get('#navigator [data-cy="Favorites"]')
      .should('exist')
      .should('be.visible')
      .contains('Favorites')
      .click();

    // expect the favorites list component to be visible
    cy.get('c8y-favorites-manager').should('exist').should('be.visible');
  });
});

Make sure you have the local server (npm run start) and Cypress (npm run cypress:open) up and running. The new test will appear in the Specs section of Cypress. The test will be executed if you click on the favorites-manager.cy.ts test suite.

cypress-test-01

The test is fairly simple, but demonstrates how to use commands from the Cumulocity Cypress package in combination with data-cy attributes. Assigning data-cy attributes to your UI elements as an identifier is considered a best practice. Unlike class or id selectors, which can change as a result of styling or structural adjustments in the codebase, data-cy ensures that elements can be consistently identified and interacted with during automated tests, improving test stability and maintenance.

We create a new test suite by using describe('Favorites Manager', () => { ... }); This test suite contains all tests related to the Favorites Manager. Before running any tests, we need to create a session and authenticate for Cumulocity. Normally, you would implement the authentication process yourself, but using the Cumulocity Cypress package you can use the existing command cy.getAuth().login() to take care of the login procedure. The getAuth() command, when called without any parameters, takes the environment variables C8Y_TENANT, C8Y_USERNAME and C8Y_PASSWORD from cypress.env.json. You can find additional information about this command in the documentation of the package.

We don’t want to use the technical user for the tests, but instead create a temporary user, who has specific permissions and roles assigned. This is helpful, if you want to test component, which require specific permissions or roles to be accessible. For simplicity, the user is defined within the test suite as a constant:

const testUser = {
    userName: 'testuser',
    password: 'ZVfJbDXuN!3t',
    displayName: 'Test User',
    email: 'test.user@softwareag.com',
  } as IUser;

In the before() block of the test suite, we authenticate to Cumulocity using the technical user defined in the cypress.env.json. Once authenticated, we use the cy.createUser() command from the Cumulocity Cypress package to create a new user, who has the role business assigned and has access to the cockpit application. If the user has successfully been created, we use this user for authentication for all coming tests in the Favorites Manager test suite in the beforeEach() block.

Important Make sure your technical user has the necessary rights to manage users

After all tests have been executed, we delete the temporary user again, who has been created before running the tests. We use the technical user again, to delete the temporary user in the after() block. We must authenticate with the technical user again, otherwise we would still use the session of the test user, who can’t be used for user management. For deleting the test user, we use the deleteUser() command:

// delete the user after the test suite runs
after(() => {
  cy.getAuth().login().deleteUser(testUser);
});

Hint You can chain commands. Chaining commands will yield the result of the previous command and provide it to the next command. Read more about it in the documentation of the Cumulocity Cypress package

The actual test it('should load favorites list when clicking on favorites menu item', () => { ... }); consists of 3 statements:

  1. Navigate to the application/page, which should be the starting point of the test. The navigation is done by using yet another Cumulocity Cypress command cy.visitAndWaitForSelector(). It extends the cy.visit() command. In addition to the URL, you can specify a selector for a component, which should be visible before the test can continue. In this test, we wait until the navigation menu on the left side is visible using its id #navigator.

  2. Once the navigation menu is visible, we check if it contains a menu item for the Favorites Manager Plugin with the label Favorites. If it exists, we trigger a click event to navigate to the corresponding component. For this step, we use standard Cypress functionality with cy.get() and should(). Noteworthy, Cumulocity adds the data-cy attributes to various HTML elements for an easier selection of these elements inside of Cypress tests. There isn’t a list of all existing data-cy attributes in Cumulocity, you must check yourself if a an HTML element is selectable via a data-cy attribute.

  3. The last statement of the test checks if the favorites list is displayed to the user by using the selector of the favorites list component: cy.get('c8y-favorites-manager').

Test workflow to favor assets

For the next test case, we want to test the functionality if an asset can be added to the favorites list and also be removed from the favorites list again. This test is a little more extensive:

it('should add a new favorite to the list and remove it again', () => {
    // open the Cockpit application extended with the Favorites Manager module and wait for the navigator menu to be visible
    cy.visit(`/apps/cockpit/index.html#`, {
      qs: { remotes: '{"sag-ps-iot-pkg-favorites-manager-plugin":["FavoritesManagerModule"]}' },
    });

    // check for the favorites menu item and click on it to navigate to the favorites list
    cy.get('#navigator [data-cy="Favorites"]', { timeout: 60000 })
      .should('exist')
      .should('be.visible')
      .contains('Favorites')
      .click();

    cy.get('c8y-favorites-manager').should('exist').should('be.visible');

    // expect the favorites list to be empty for the newly created user
    cy.get('c8y-favorites-manager [data-cy="favorites-empty-state"]')
      .should('exist')
      .should('be.visible');

    cy.visit(`/apps/cockpit/index.html#/group/${assetId}`, {
      qs: { remotes: '{"sag-ps-iot-pkg-favorites-manager-plugin":["FavoritesManagerModule"]}' },
    });

    cy.intercept('PUT', '/user/currentUser').as('addFavoriteForUser');

    // check for the favorites action button its state and click on it
    // asset isn't a favorite yet and the button should contain the text "Add to favorites"
    cy.get('[data-cy="favorites-action-button"]', { timeout: 60000 })
      .should('exist')
      .should('be.visible')
      .contains('Add to favorites')
      .click();

    cy.wait('@addFavoriteForUser').its('response.statusCode').should('eq', 200);

    // expect the button to change its state to "Remove from favorites"
    cy.get('[data-cy="favorites-action-button"]')
      .should('exist')
      .should('be.visible')
      .contains('Remove from favorites');

    // navigate back to the favorites list and expect the list to contain the favorite
    cy.get('#navigator [data-cy="Favorites"]')
      .should('exist')
      .should('be.visible')
      .contains('Favorites')
      .click();

    cy.get('c8y-favorites-manager').should('exist').should('be.visible');

    // expect the favorites list not to be empty for the newly created user
    cy.get('c8y-favorites-manager [data-cy="favorites-empty-state"]').should('not.exist');

    // expect the favorites list to contain the favorite
    cy.get(
      'c8y-favorites-manager [data-cy="favorites-list"] [data-cy="c8y-data-grid--row-in-data-grid"]'
    )
      .should('have.length', 1)
      .within((favoriteRow) => {
        cy.wrap(favoriteRow).get('[data-cy="data-grid--System ID"]').contains(assetId);

        // navigate to the device detail page by clicking on the favorite
        cy.wrap(favoriteRow).get('[data-cy="data-grid--Name"] a').should('exist').click();
      });

    // check for the favorites action button its state and click on it
    // asset is a favorite and the button should contain the text "Remove from favorites"
    cy.get('[data-cy="favorites-action-button"]')
      .should('exist')
      .should('be.visible')
      .contains('Remove from favorites')
      .click();

    // wait for update on user object
    cy.wait('@addFavoriteForUser').its('response.statusCode').should('eq', 200);

    // expect the button to change its state to "Add to favorites"
    cy.get('[data-cy="favorites-action-button"]')
      .should('exist')
      .should('be.visible')
      .contains('Add to favorites');

    // navigate back to the favorites list and expect the list to be empty
    cy.get('#navigator [data-cy="Favorites"]')
      .should('exist')
      .should('be.visible')
      .contains('Favorites')
      .click();

    cy.get('c8y-favorites-manager').should('exist').should('be.visible');

    // expect the favorites list to be empty again for the newly created user
    cy.get('c8y-favorites-manager [data-cy="favorites-empty-state"]')
      .should('exist')
      .should('be.visible');
  });

We start the test similar to the previous one. We open the Cumulocity Cockpit application extended by the Favorites Manager Plugin locally and navigate to the ‘Favorites list’ via the left navigation menu. This time we explicitly check if the ‘Favorites list’ is empty, because we have created a completely new user and expect there are no favorites yet stored for this new user. We use a mix of component selectors and data-cy selectors to identify the required UI elements:

// expect the favorites list to be empty for the newly created user
cy.get('c8y-favorites-manager [data-cy="favorites-empty-state"]')
  .should('exist')
  .should('be.visible');

In the next step, we navigate to an asset (can be a device, a group or a DTM asset) to add it to our ‘Favorites list’. For simplicity, we have hardcoded the id of the asset in the test. Normally, you would dynamically look up the asset for the test.

Every time an asset is added to or removed from the ‘Favorites list’, the plugin will send a PUT request to the Cumulocity’s /user/currentUser API. We will intercept this request to check if it has been sent and if it returns the correct response code:

 // listen for the request to update the favorite list in the user object
 cy.intercept('PUT', '/user/currentUser').as('addFavoriteForUser');

Once the detail page of the asset has been loaded, we check if the action button to add the asset to the ‘Favorites list’ is visible and displays the correct label. If this is the case, we click on the button to trigger the process to add the current asset to the user’s ‘Favorites list’. In addition, we check whether the associated request has been sent and responded with the expected response code. Finally, we check if the action button has been updated correctly and now displays the label to remove the asset from the ‘Favorites list’:

// check for the favorites action button its state and click on it
    // asset isn't a favorite yet and the button should contain the text "Add to favorites"
cy.get('[data-cy="favorites-action-button"]', { timeout: 60000 })
  .should('exist')
  .should('be.visible')
  .contains('Add to favorites')
  .click();

// wait for the update on the user object
cy.wait('@addFavoriteForUser').its('response.statusCode').should('eq', 200);

// expect the button to change its state to "Remove from favorites"
cy.get('[data-cy="favorites-action-button"]')
  .should('exist')
  .should('be.visible')
  .contains('Remove from favorites');

Next, we navigate back to the ‘Favorites list’, because we want to chek if the asset is now displayed as part of the ‘Favorites list’. We count the rows of the ‘Favorites list’ and expect its length to equal to 1. If it is the case, we check for 1 row found, whether the System ID matches the id of the asset, which we have favored. If this test is successful as well, we navigate back to the asset by clicking on its name using the Name column.

// expect the favorites list to contain the favorite
cy.get(
  'c8y-favorites-manager [data-cy="favorites-list"] [data-cy="c8y-data-grid--row-in-data-grid"]'
)
  .should('have.length', 1)
  .within((favoriteRow) => {
	cy.wrap(favoriteRow).get('[data-cy="data-grid--System ID"]').contains(assetId);

	// navigate to the device detail page by clicking on the favorite
	cy.wrap(favoriteRow).get('[data-cy="data-grid--Name"] a').should('exist').click();
  });

Back on the detail page of the asset, we remove the asset from the ‘Favorites list’ again. We validate if the removal on the detail page was successful by navigating back to the ‘Favorites list’ to check if the list is now empty again.

cypress-test-02

Conclusion

The Cumulocity Cypress package significantly streamlines the process of writing e2e tests for Cumulocity applications or plugins with Cypress. By utilizing this package, you can bypass the repetitive coding of common tasks and commands, such as logging into Cumulocity or creating new users equipped with specific permissions and roles. The package is under active development, ensuring it is continually updated with new Cypress commands to further improve its utility. For a comprehensive understanding of its capabilities, I encourage you to explore the documentation. If you are missing a specific command or you want to contribute to the package, use the issue section.

5 Likes