E2E with Cypress

Cypress is an end-to-end web app testing framework with automatic waiting, live reloads, and multiple browser support.

Cypress in @iqmetrix/cra-template

Note that Cypress is already configured if you created your app with @iqmetrix/cra-template.

Global Cypress Configuration#

  1. Install Cypress, Mocha Reporter and utility packages

    npm install -D cypress mocha-junit-reporter mocha start-server-and-test @iqmetrix/cypress
  2. Bootstrap your Cypress config. This command will create samples for E2E and unit tests, along with examples of mocking APIs (fixtures).

    npx cypress run
  3. Add the generated Cypress files to .gitignore file

    .gitignore
    cypress/results
    cypress/screenshots
    cypress/videos
  4. Import @iqmetrix/cypress.

    cypress/support/index.js
    import "@iqmetrix/cypress";
  5. Configure Cypress to find your running application and report in JUnit format.

    Refer to the full Cypress configuration documentation for more details.

    cypress.json
    {
    "baseUrl": "http://localhost:3000/",
    "reporter": "mocha-junit-reporter",
    "reporterOptions": {
    "mochaFile": "cypress/results/[hash].junit.xml",
    "toConsole": true,
    "attachments": true
    }
    }
  6. Configure your auth and client application credentials

    Cypress needs to log in in order to interact with your application. You'll need to provide it the following settings in order for it to authenticate. For the clientId, you should use the one associated to your backend service. If you are unsure of what all this means, please refer to the Auth service documentation.

    cypress.json
    {
    "env": {
    "auth": {
    // The test user that cypress will login into Hub as
    "username": "YOUR_TEST_USERNAME",
    "password": "YOUR_TEST_USER_PASSWORD",
    // Get these from the Dev Console in Hub when logged in via your iQmetrix creds
    "clientId": "YOUR_TEST_CLIENT_ID",
    "clientSecret": "YOUR_TEST_CLIENT_SECRET",
    "envSuffix": "int" // "int" | "rc" | ""
    }
    }
    }
Secure credential storage

We recommend storing secrets and credentials in a more access controlled area rather than storing them in your cypress.json and checking them into source control. If you'd like to go down this route, see here;

Additional configuration for CRA/typescript projects#

For more information, refer to the official Cypress CRA guide

  1. Add the following dependencies

    npm install --save-dev webpack @bahmutov/add-typescript-to-cypress
  2. Update the types for your cypress/plugins/index.js

    cypress/plugins/index.js
    /// <reference types="cypress" />
    /**
    * @type {Cypress.PluginConfig}
    */
    module.exports = (on, config) => {
    // `on` is used to hook into various events Cypress emits
    // `config` is the resolved Cypress config
    };
  3. Finally, add a new tsconfig.json under the cypress folder

    cypress/tsconfig.json
    {
    "extends": "../tsconfig.json",
    "include": [
    "../node_modules/cypress",
    "../node_modules/@iqmetrix/cypress",
    "*/*.ts"
    ],
    "compilerOptions": {
    "noEmit": false,
    "isolatedModules": false
    }
    }

    If you are using eslint, you will need to remove the parserOptions.project configuration if set.

Configure Scripts#

If you wish to have your Cypress tests run during the build step in your CI/CD, you can modify your test task to run them like below. Otherwise, adding the test:e2e alone for local usage is sufficient!

  1. For React apps

    Remember to update the TODO_APP_NAME with your app path url.

    package.json
    {
    "homepage": "/TODO_APP_NAME",
    "scripts": {
    "cy:open": "cypress open",
    "cy:run": "cypress run",
    "test": "npm run test:unit && npm run test:e2e",
    "test:unit": "react-scripts test --ci --watchAll=false",
    "test:e2e": "start-server-and-test start http-get://localhost:3000/TODO_APP_NAME cy:run"
    }
    }
  2. For Hub v1 apps

    package.json
    "scripts": {
    "cy:open": "cypress open",
    "cy:run": "cypress run",
    "serve:prod": "hub-scripts serve --prod",
    "test": "start-server-and-test serve:prod http://localhost:3000 cy:run"
    }

Most of Hub v1 apps have no unit tests set-up. If you have it on your app, follow the same scripts of React apps configuration, changing only the test:unit and test:cypress scripts.

Write Tests#

Refer to the official Cypress documentation for full usage examples.

Writing test for Typescript projects

When writing tests for Typescript projects, you can use the extension .ts for your test files. This allows you to test the typing of your components and functions.

cypress/integration/example.test.js
describe("App", () => {
before(() => {
// Login with environment variables
cy.login(Cypress.env("auth"));
// Provide host-interop handlers
cy.addHostInteropHandlers();
});
after(() => {
cy.logout();
cy.removeHostInteropHandlers();
});
beforeEach(() => {
cy.visit("/");
});
it("Should load the page", () => {
cy.get(".heading-name").should("contain", "Hub Boilerplate");
});
it("Should show the alert", () => {
cy.get(".x-sayhello-button").click();
cy.get(".messenger-message-inner")
.should("be", "visible")
.should("contain", "Hello again!");
});
});

Storing Credentials Securely#

Keeping secrets out of source control is a good practice (even if they are just for the test environment!). In the example above, we're pulling credentials from environment variables via cy.login(Cypress.env("auth")). There are two methods of keeping your secrets in a safe place while allowing your Cypress tests to use them: Pipeline environment variables or Azure Key Vault.

Pipeline Environment Variables#

You can set environment variables on the Pipeline with the ability to keep them secret in Azure DevOps.

  1. Set these environment variables in your Pipeline and local environment. Environment variables can be set directly on the Pipeline in the Azure DevOps Pipeline or you can create a Variable Group and have the Pipeline use that Variable Group. You may also make them 'secret' as well! Cypress will strip the CYPRESS_ and inject everything else into the Cypress.env(). See here for more details;
CYPRESS_clientSecret=REPLACE_WITH_YOUR_VALUE
CYPRESS_password=REPLACE_WITH_YOUR_VALUE
  1. Update your cy.login call in your tests
cy.login({
username: "test.user123",
password: Cypress.env("password"),
clientId: "your.client.id",
clientSecret: Cypress.env("clientSecret"),
envSuffix: "int"
});

Azure Key Vault#

Prerequisites: You must have an Azure subscription and a Key Vault resource to use this plugin.

Alternatively, you can store your secrets/credentials in Azure Key Vault and use the cy.loginWithKeyVaultCredentials method. In order for Cypress to access Key Vault, you'll need to create a Service Principal and authorize the Service Principal to get secrets from your Key Vault.

  1. Configuring Key Vault

Use the Azure Cloud Shell snippet below to create/get client secret credentials.

  • Set correct account subscription:
az account set --subscription "your-account-subscription"
  • Create a service principal and configure its access to Azure resources:
az ad sp create-for-rbac -n <your-application-name> --skip-assignment
  • Grant the above mentioned application authorization to perform secret operations on the keyvault:
az keyvault set-policy --name <your-key-vault-name> --spn $AZURE_CLIENT_ID --secret-permissions backup delete get list purge recover restore set

Set environment variables to authenticate with your Azure Key Vault. These can all be found through the Azure Portal under Azure Active Directory > App registrations. The Client ID and Tenant ID can be found in Summary and Client Secret is under Certificates & secrets. For example, you can access hubtests using information from its applications registration.

AZURE_CLIENT_ID="generated-app-ID"
AZURE_CLIENT_SECRET="random-password"
AZURE_TENANT_ID="tenant-ID"
Enviroment Variables on Azure

Don't forget to set these environment variables on your Pipeline too! Environment variables can be set directly on the Pipeline in the Azure DevOps Pipeline or you can create a Variable Group and have the Pipeline use that Variable Group.

  1. Register the Cypress plugin
cypress/plugins/index.js
const { credentialsPlugin } = require("@iqmetrix/cypress/dist/plugins");
module.exports = (on, config) => {
return credentialsPlugin(on, config);
};
  1. Create a test user or use an existing one

  2. Use the commands in your tests to authenticate test users

describe("Example Azure Key Vault Flow", () => {
before(() => {
cy.getKeyVaultCredentials({
envSuffix: "int",
vault: "hubtests",
user: "demo",
}).then(cy.login);
// or //
cy.loginWithKeyVaultCredentials({
envSuffix: "int",
vault: "hubtests",
user: "demo",
});
});
});

Mocking APIs#

If you want to test only the flow and mock APIs or if your API is not ready and you want to write tests and not wait for it, you can create a custom command to mock app APIs.

  1. Create the custom command for mocking APIs

    cypress/support/mockApi.js
    Cypress.Commands.add("mockApi", (skip = []) => {
    // if you want to skip one or more mock for an specific test, you can send the list in the command parameter
    cy.server();
    const routes = {
    me: ["**/me*", "fixture:me.json"],
    entity: ["**/v1/enabled/entity**", "fixture:entity.json"],
    };
    // the object keys will be the route/api alias
    Object.keys(routes)
    .filter(route => skip.indexOf(route) === -1)
    .forEach(route => {
    cy.route("GET", routes[route][0], routes[route][1]).as(route);
    });
    });
  2. Import it in your cypress suite

    cypress/support/index.js
    import "./mockApi";
  3. Add the mocked response for your API into the fixtures folder

    cypress/fixtures/me.json
    {
    "Id": 10001,
    "ParentEntityId": 21090,
    "FirstName": "test",
    "LastName": "test",
    "Email": "test@wong.com",
    "UserName": "test@Core5Daily",
    "IsExternal": false,
    "Picture": null,
    "SecurityRequirements": {}
    }
  4. Use the created mockApi command into your tests

    cypress/integration/example2.test.js
    describe("Navigation", function () {
    before(() => cy.login(Cypress.env("auth")));
    after(() => cy.logout());
    it("Should load the page with mock data", () => {
    cy.mockApi();
    cy.visit("/");
    cy.get("#user-email").should("contain", "test@wong.com"); // returns true
    });
    it("Should load the page", () => {
    cy.visit("/");
    cy.get("#user-email").should("contain", "test@wong.com"); // returns false
    // could use cy.mockApi('me') to not mock the /me endpoint
    });
    });

Notes#

Browser Support in Cypress 4#

Cypress 4 marks a significant milestone in development that will continue to elevate the testing experience for everyone. Release notes contains a broader list of breaking changes, features and bug fixes, but the following highlights we consider being important:

Persist authentication tokens between tests#

By default Cypress does not preserve localStorage between tests. This requires you to explicitly restore the token between the tests when using older versions of @iqmetrix/get-authentication-token

before(() => cy.login(credentials));
beforeEach(() => cy.restoreToken());
Last updated on by Steven Lu