# Testing

# Teststrategie in hubble und Einleitung in Cypress

hubble's Teststrategie besteht primär daraus, reales User Verhalten in Tests nachzustellen, was im Folgenden auch als User Flow bezeichnet werden soll. Somit soll gewährleistet werden, dass die verschiedenen Aspekte wie UI Elemente, sowie API Responses wie erwartet funktionieren. Dafür wird das Testing Tool Cypress (opens new window) verwendet. Um also die korrekte Funktionsweise der unterschiedlichen Programmteile zu testen, existiert momentan kein Stubbing von Network Calls (opens new window). Jedoch sollte beachtet werden, dass es bei nicht lokalen Servern zu einer Überlastung des Servers kommen kann. Falls es außerdem implementierte Middleware wie Rate Limiter oder ähnliches gibt, dann würde dies, bei einer großen Menge von Anfragen, ebenfalls einen Nachteil darstellen. In solchen Fällen kann Stubbing zu einer erfolgreichen Teststrategie führen. Die Möglichkeiten, die Cypress dafür anbietet, werden in der offiziellen Dokumentation im Abschnitt Network Requests (opens new window) erläutert.

Außerdem ist es möglich, einen bestimmten State auch programmatisch zu erreichen und somit einen Aufbau über UI Interaktionen zu umgehen. Da die Tests, wie oben beschrieben, jedoch so genau wie möglich User Verhalten abbilden sollen, wurde diese Funktionalität bisher nicht verwendet. Dem Anspruch entsprechend werden auch die Selektion von UI Elementen oder die Abfrage nach Existenz eines bestimmten Elementes, soweit wie möglich, über den sichtbaren Text vorgenommen. Dabei können in den Tests Assertions mit Hilfe der in Cypress eingebundenen Chai Methoden (opens new window), entweder im TDD (opens new window) oder BDD (opens new window) Stil, erstellt werden und diese mit den eingebundenen Mocha Methoden (opens new window) (describe, it, context, etc.) in Blöcke organisiert werden.

Beim Schreiben von Tests in Cypress sollte beachtet werden, dass die Kombination aus Cypress API Commands und den Assertion Methoden, das gewünschte Verhalten haben, um deterministisches Testverhalten zu gewährleisten. Denn es existieren automatische Mechanismen zur Wiederholung von Befehlen (Retry-ability (opens new window)). Dabei erlauben unterschiedliche Rückgabewerte, unterschiedliches Command Chaining (opens new window). Welche Werte zurückgeliefert werden, kann in der API Dokumentation (opens new window), je Command, im Unterpunkt Yields nachgelesen werden. Außerdem sind Cypress Commands asynchron und müssen unter diesem Wissen verwendet werden (Mixing Async and Sync code (opens new window)).

TIP

Je nach den bestehenden Ansprüchen, Gegebenheiten und Funktionalitäten sollte immer eine passende Teststrategie gewählt werden.

# Wichtige Dateien und bestehende Tests

Der beschriebenen Teststrategie entsprechend, sind Tests in hubble also nach User Flow aufgeteilt. Jeder User Flow wird dabei sowohl für Desktop, als auch für mobile Resolutionen getestet. Die Werte der Resolutionen sind dabei als Environment Variable in der ~/cypress.json aufgelistet:

// ~/cypress.json (simplified)
"env": {
    "resolutions": {
        "mobile": {
            "viewportWidth": 375,
            "viewportHeight": 667,
            "desktop": false
        },
        "desktop": {
            "viewportWidth": 1024,
            "viewportHeight": 768,
            "desktop": true
        }
    },
    // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Zur Verwendung, der im Ordner ~/cypress/ befindlichen Tests, werden die Werte aus der ~/cypress.json in der ~/support/utils.js aufbereitet:

// ~/cypress/support/utils.js
const resolutions = Cypress.env('resolutions');


const mobile = {
    viewportWidth: resolutions.mobile.viewportWidth,
    viewportHeight:resolutions.mobile.viewportHeight,
    desktop: resolutions.mobile.desktop,
}


const desktop = {
    viewportWidth: resolutions.desktop.viewportWidth,
    viewportHeight: resolutions.desktop.viewportHeight,
    desktop: resolutions.desktop.desktop,
}


export const viewPortSizes = [desktop, mobile];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

Da es bei einem Shop, wie im Abschnitt Seitentypen beschrieben, auch viele Seiten und Use Cases gibt, die einen eingeloggten Benutzer voraussetzen und die korrekte Funktionalität von Login und Registrierung ausschlaggebend ist für Seitenbesucher, sollten die entsprechenden User Flows getestet werden. Hier existieren bereits einige Tests in hubble: Die Generierung von User Daten, die für die Bereitstellung eines eingeloggten Users benötigt werden, findet über faker.js (opens new window) statt. Um auch für Livetests, die gegen einen Live-Server laufen, ein erkennbares Emailformat zu gewährleisten, existiert folgende Environment Variable:

// ~/cypress.json
"env": {
    "emailBase": "<ENTER-YOUR-PREFIX-HERE>-$placeholder@<ENTER-YOUR-DOMAIN-HERE>.com"
}
1
2
3
4

Der Abschnitt $placeholder wird dabei von den von faker.js (opens new window) generierten Daten ersetzt und sollte somit nicht editiert werden. Im Folgenden ist die Verwendung der Environment Variable emailBase zu sehen:

// ~/support/utils.js
const faker = require('faker');

export function getRandomEmail () {
    const emailBase = Cypress.env('emailBase');

    faker.locale = "en"; // Emails dürfen keine Umlaute enthalten

    const randomSuffix = `${faker.name.firstName()}${faker.name.lastName()}`;

    return emailBase.replace('$placeholder', randomSuffix);
}
1
2
3
4
5
6
7
8
9
10
11
12

Diese Funktion, die emailBase verwendet, befindet sich ebenfalls in der ~/support/utils.js, da diese für alle Tests zur Verfügung stehen soll.

Auf die gleiche Art werden auch Passwörter oder weitere Account Daten mit faker.js (opens new window) generiert:

// ~/support/utils.js
export const getRandomPw = () => faker.internet.password();
1
2

Beispielsweise gilt es für den Checkoutprozess ohne eingeloggten Benutzer, trotzdem ein Formular auszufüllen, welches, die für die Bestellung relevanten Einträge erwartet. Diese lassen sich wie folgt generieren:

// ~/support/utils.js
export function getGuestData () {
    const guestEmail = getRandomEmail();

    faker.locale = "de"; // die Email darf keine Umlaute enthalten, die folgenden Daten schon

    const guestFirstName = faker.name.firstName();
    const guestLastName = faker.name.lastName();
    const guestStreet = faker.address.streetAddress();
    const guestZipCode = faker.address.zipCode();
    const guestCity = faker.address.city();

    return {
        guestEmail,
        guestFirstName,
        guestLastName,
        guestStreet,
        guestZipCode,
        guestCity
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

Diese Hilfsfunktion und ihre Rückgabewerte lassen sich nun in der ~/cypress/integration/buy_product_guest_flow.js, die den Gast Checkout Flow testet, verwenden:

import { getGuestData, selectAnOption, viewPortSizes } from "../support/utils"


const { guestEmail, guestFirstName, guestLastName, guestStreet, guestZipCode, guestCity } = getGuestData()
1
2
3
4

Ein Ausschnitt zu der konkreten Verwendung der eingebundenen Werte sieht wie folgt aus:

describe('Buy Product Guest Flow', function () {
    viewPortSizes.forEach(viewport => {

        describe(`Tests for ${viewport.viewportWidth} w x ${viewport.viewportHeight} h`, function () {

            beforeEach(() => {
                cy.viewport(viewport.viewportWidth, viewport.viewportHeight)
            })

            it('selects a category', function () {
                cy.acceptCookies()

                cy.pickCategory(viewport.desktop)
            })

            it('enters guest customer data', function () {
                cy.get('#email')
                    .type(guestEmail)
                    .should('have.value', guestEmail)
            
                cy.get('#firstName')
                    .type(guestFirstName)
                    .should('have.value', guestFirstName)
            
                // ...
            })
        })
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

In dem Testfall it('selects a category', function () {} befinden sich dabei zwei Funktionsaufrufe, die noch nicht erläutert wurden. Es handelt sich hierbei um sogenannte Custom Commands (opens new window), die beispielweise ähnlich zu cy.get() wiederverwendet werden können und somit also wiederverwendbare Testlogik an einer Stelle bündeln. Einer der wichtigen Custom Commands, die in hubble existieren, ist cy.login(). Wie beschrieben gilt es für einige Shop Seiten eingeloggte Benutzer bereitzustellen, wofür erst eine Registrierung und dann jeweils immer derselbe Login Prozess erfolgen muss. Daher ist dies ein geeigneter Fall für einen Custom Command. Dabei sollten Custom Commands sich in der ~/cypress/support/commands.js befinden, da dessen Inhalte automatisch in Testdateien, auch als Spec Files geläufig, zur Verfügung stehen.

Cypress.Commands.add("login", (email, password, desktop) => {
    if (!desktop) cy.get('.customer-account-cpt-wrp').click()


    register(true, desktop, email, password)


    cy.get('#email')
        .type(email)
        .should('have.value', email)


    cy.get('#password')
        .type(password)
        .should('have.value', password)


    cy.get('form')
        .find('button')
        .contains('Login')
        .click()


    cy.contains('Logout')
    
    // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

Für eine vollständige Liste der in hubble verfügbaren Custom Commands, kann die ~/cypress/support/commands.js (opens new window) referenziert werden.

# Deaktivieren von einzelnen Testfällen innerhalb einer Spec Datei

Es besteht die Möglichkeit einzelne Tests bei Bedarf zu deaktivieren, wodurch diese übersprungen werden im Testdurchlauf, jedoch trotzdem, in einer gesonderten Farbe, mit aufgelistet werden.



















 




















// ~/cypress/integration/buy_product_flow.js (simplified)
describe(`Tests for ${viewport.viewportWidth} w x ${viewport.viewportHeight} h`, function () {
    // ...

    it('selects a product & adds to cart', function () {
        cy.get('.listing-wrp .listing-item .product-card')
            .should('be.visible')
            .pickRandomProduct()
    
    
            cy.contains('Add to Cart').click()
    
    
            cy.contains('Successfully added item to cart.')
            cy.contains('Shopping Cart')
            cy.contains('Keep shopping')
    })

    it.skip('goes to shopping cart & goes to checkout', function () {
        cy.contains('Shopping Cart')
            .should('exist')
            .click()
    
    
            cy.url()
                .should('include', '/checkout/cart')
                .wait(800)
    
    
            cy.contains('Go to checkout')
                .should('exist')
                .click()
    
    
            cy.url()
                .should('include', '/checkout/shopware-onepage')
    })
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# Übersicht zu der Ordnerstruktur von ~/cypress/

Eine detaillierte Beschreibung der Funktionsweise der unterschiedlichen Dateien kann in der offiziellen Dokumentation im Abschnitt Writing and Organizing Tests (opens new window) nachgelesen werden.

# Konfiguration

Um die in hubble existierenden Tests auszuführen, ist es notwendig, für die zu generierenden Emails einen Prefix und eine Domain anzugeben:

{
   "emailBase": "<ENTER-YOUR-PREFIX-HERE>-$placeholder@<ENTER-YOUR-DOMAIN-HERE>.com"
}
1
2
3

# Cypress Tests Starten

Es existieren zwei Möglichkeiten Cypress Tests zu starten: Zum einen ist es möglich den visuellen Ablauf zu verfolgen und nach Testdurchlauf jeden Schritt zu untersuchen.

npx cypress open 
1

In dieser Variante sind Spec Dateien einzeln auszuwählen und es wird daraufhin ein Browserfenster mit dem Test geöffnet und automatisch gestartet.

Zum anderen ist es möglich Tests direkt über das Terminal durchlaufen zu lassen, was sich auch für einen Continuous Integration Prozess eignet:

npx cypress run 
1

Bei diesem Befehl werden jedoch alle Spec Dateien, die sich im ~/cypress/integration/ Ordner befinden, durchlaufen. Da dies oft nicht notwendig ist und längere Zeit beansprucht, als einen einzelnen relevanten Test durchzuführen, gibt es auch bei der Verwendung des Terminals eine Möglichkeit eine einzelne Datei aufzurufen:

npx cypress run --spec "cypress/integration/<SPEC>.js" 
1