Lambda IT Blog

Best practices for E2E Tests with Cypress and Angular

Publiziert am 07. May 2020 von Matthias Baldi

In the first Cypress blog article we described a way to set up an Angular project with Cypress and Typescript.
In this article I will show a possible architecture in order to reuse much of the test code and how to minimize code changes by holding the fixtures or selectors externally.

Tooling:

  • Cypress 4
  • Angular 9+, with matching Typescript
  • Chrome 80+
  • Visual Studio Code

Prerequisites:

  • Installed NodeJS
  • Installed Angular CLI
  • If you have a proxy, you need to configure the proxy for node, Cypress will download some executables after its installation.
  • Cypress Setup in your Angular Project

Structure your Tests

Folder Setup

In the following chapters we will go through each folder and fill the files with content.
At the end, you will get a folder structure like this:

cypress
├── fixtures
│   └── sample.spec.json
├── integration
│   └── sample.spec.ts
├── plugins  # installed & created automatically
│   ├── ...
├── support
│   ├── commands.ts
│   ├── index.js
│   ├── po
│   │   └── sample.po.ts
│   └── shared
│       └── common.ts
└── tsconfig.json

Folder: Support

In here you should structure your shared code and methods you will use to manipulate your app.
It determines what app you want to test or which architecture it has. You can structure your shared code into a po (for page object) folder. Add a new file for every component or page and use it for the specific methods e.g. clickCalculateButton() {OR}.

Naming PO

We decided on the PO name because we had previously used Protractor and the component abstractions were kept in PO files.

Basically you can name these folders and files whatever you like; this is not defined by Cypress.

We create a folder shared for all the common methods we can use multiple times over the complete application, i.e. openBrowser() or navigateToRoute(myRouteName). If you want to use your test code in multiple projects, you may have to write plugins or share it over npm packages.
In our case, based on the first article, the common.ts file would include the open method for the browser, the findAndClick method is moved into a PO file.

// support/shared/common.ts
export namespace Common {
    export function open(
        host: string = 'http://localhost',
        port: number = 4200
    ) {
        cy.visit(`${host}:${port}`);
        // may do here your login or other stuff
    }
}

To complete this change, remove the findAndClick method in the commands.ts:

// support/commands.ts
import { Common } from './shared/common';

declare global {
    namespace Cypress {
        interface Chainable {
            /**
             * Open browser and navigate
             *
             * @param [host='http://localhost'] hostname where your app is running
             * @param [port=4200] number of port where your app is running
             */
            open(host?: string, port?: number): Chainable<Element>;
        }
    }
}

// collection of cy enabled methods
Cypress.Commands.add('open', Common.open);

Because our default generated Angular app has only one page we create a sample.po.ts under the po folder and prefill it with our checkTextInTerminal. These methods were used as a direct Cypress call in the base tutorial. Technically this is absolutely okay, but we may want to check the terminal text multiple times. That is why we wrap this line into a method with a text parameter.

// support/po/sample.po.ts
export namespace SamplePo {
    const sampleFixtureJson = 'sample.spec.json';

    export function checkTextInTerminal(text: string) {
        cy.fixture(sampleFixtureJson).then((sample) => {
            cy
                .get(sample.terminal)
                .should('contain.text', text);
        });
    }

    export function findButtonAndClick(buttonText: string) {
        cy.fixture(sampleFixtureJson).then((sample) => {
            cy
                .get(sample['button-container'])
                .contains(buttonText)
                .click();
        });
    }
}

The most important advantage of these PO files is, that you only have to change the fixtures when a selector is changing during development.
When more than just a selector is changing, maybe a whole feature, then you have to change the PO file. But in any case you do not have to change the test itself.

This separation of logic allows, for example, a tester to write the actual tests under the integration folder and the developer will implement the logic in the PO files.

Cypress Fixtures Loading

Cypress has multiple ways of loading a fixtures file. These ways are documented here.
Decide for your project and for your test cases which one is best.

Folder: Integration

The structure of this folder depends on your project size and architecture. If you want to create multiple files per component it is useful to have a clear structure. Create a subfolder per feature or component to test. In our case we will only create one file in a flat structure.

// integration/sample.spec.ts
import { SamplePo } from '../support/po/sample.po';

describe('Angular Welcome Board', () => {
    before(() => {
        cy.open();
    });

    it('able to click buttons', () => {
        SamplePo.findButtonAndClick('Angular Material');
        SamplePo.checkTextInTerminal('ng add @angular/material');

        SamplePo.findButtonAndClick('Run and Watch Tests');
        SamplePo.checkTextInTerminal('ng test');
    });
});

What you can now see is, that we moved all the fixtures/selector information away from the real test source, so you are left with a human readable test.

Because the Angular default app is missing better selectors we select the buttons by text. This is not a recommended behaviour and you should use CSS classes or ID's to select your buttons.
You can find Best Practices for Selecting Elements here.

Folder: Fixtures

In the Fixtures folder it would be good if the structure was similar to the Integration folder.
So you can add a different fixtures file including the selectors for each component or view.

If you have JSON objects for REST API's or images to test an upload form you can move these artefacts into a folder like assets/images or assets/api.

To complete our test scenario for the default generated Angular app, we have to create the sample.spec.json file with the following content:

// fixtures/sample.spec.json
{
    "terminal": "div.terminal",
    "button-container": ".card.card-small"
}

When you now change selectors during the further development, you only have to change it here.
Your test should run exactly the same way as before.

Conclusion

Separating the logic, selectors and actual test files reduces the amount of work necessary to keep the tests up to date during development and allows a tester to participate in the development team.
Cypress makes it very easy to load many different fixtures filetypes during a running test and you can test different scenarios. It is possible to automate file uploads pretty easily and even to test complex REST API calls is possible.

There is probably no ready-made solution, but the example shown here may be scalable enough for different project sizes to develop E2E tests with Cypress efficiently.

Aktuelles im 2020

Kontaktformular

Haben Sie Fragen?

Haben Sie ein spannendes Projekt oder brauchen Sie Unterstützung beim Lösen einer Herausforderung?

Wir freuen uns auf Ihre Kontaktaufnahme.

Lambda Team

Swiss Made Software

Die Lambda IT bekennt sich zum Standort Bern und Schweiz und entwickelt nicht nur zu 100% alle Software in der Schweiz, sondern versucht auch, wenn immer möglich, die lokalen Partner und Industrien zu berücksichtigen.