Bevor eine Applikation veröffentlicht wird, sollte sie umfassend getestet werden. Schließlich möchte man ja nicht, dass erst die Nutzer die Bugs finden. Nun gibt es verschiedene Arten wie man testen kann:

Functional testing

  • ...
  • Interface / UI testing
  • ...

Non-functional testing

  • ...

Cypress.io

Ich habe mich dann erstmal mit den UI Tests beschäftigt. Damit kann ich schon viel abdecken und man sieht das Problem gleich. Win Win also.

Durch meine Recherche bin ich dann auf cypress.io aufmerksam geworden.

Cypress kann direkt als NPM Module oder als Standalone installiert werden. Ersteres wird aber empfohlen:

npm install cypress

Bevor es Cypress gab, war es schwerer UI Tests zu schreiben. Man musste erstmal ein geeignetes Framework herausfinden (Mocha, Jasmine, QUnit, Karma), eine Assertion Library finden (Chai, Expext.js), Selenium installieren und den richtigen Wrapper finden und vielleicht noch andere Bibliotheken hinzufügen. Erst dann konnte man richtig mit dem Testen anfangen.

Mit Cypress wird das aber einfacher, da alles gebündelt ist und man nur das fertige Paket installieren muss.

Quelle: https://www.cypress.io/how-it-works

Preise

Bei Cypress bezahlt man nur, wenn man das Cypress Dashboard benutzen möchte. Dort kann man Tests laufen lassen, Test Recording abspeichern und fehlerbehaftete Tests fixen. Auf https://www.cypress.io/pricing findet man die aktuellen Preise. Sie sind nach der Anzahl der Test Recordings gestaffelt.

Der erste Test

Da ich aber Gitlab habe und dort auch Dateien archivieren kann, brauche ich das Cypress Dashboard nicht und benutze einfach den Cypress Runner. Der ist frei verfügbar und OpenSource.

Wenn der Cypress Runner das erste Mal installiert wird, wird automatisch eine Ordnerstruktur angelegt. Zusätzlich wird auch eine cypress.json als Konfiguration hinterlegt. Darauf gehe ich aber später ein.

Ordnerstruktur von Cypress

In diesem Beitrag werde ich nur auf den "integration" Ordner eingehen. Dort liegen nämlich die Tests. Einer davon beschäftigt sich mit den Login in meiner App:

// cypress/integration/login.spec.js
describe('login', () => {
  const userName = 'jimtim' + new Date().getTime();
  const email = userName + '@test.de';
  const password = 'password';

  before(() => {
    visitLoginPage();
    signupWith(userName, email, password, password);
    clearStorage();
  });

  beforeEach(() => {
    clearStorage();
    visitLoginPage();
  });

  afterEach(() => {
    clearStorage();
  });

  it('A user logs in and sees a blank page with the Logout button and logs out', () => {
    loginWith(userName, password);
    logout();
  });
});

const visitLoginPage = () => {
  cy.visit('http://localhost:8080');
};

const clearStorage = () => {
  localStorage.removeItem('userTokenMap');
  localStorage.removeItem('user');
};

const loginWith = (username, password) => {
  if (username.length > 0) {
    cy.get('[name="username"]').type(username);
  }

  if (password.length > 0) {
    cy.get('[name="password"]').type(password);
  }
  cy.get('button').click();
};

const signupWith = (username, email, password, confirmpassword) => {
  cy.get('[value="signup"]').click();
  if (username.length > 0) {
    cy.get('[name="username"]').type(username);
  }

  if (email.length > 0) {
    cy.get('[name="email"]').type(email);
  }

  if (password.length > 0) {
    cy.get('[name="password"]').type(password);
  }
  if (confirmpassword.length > 0) {
    cy.get('[name="confirmpassword"]').type(confirmpassword);
  }
  cy.get('button').click();

  expect(cy.contains('User saved')).to.exist;
  logout();
};

const logout = () => {
  expect(cy.get('#openDrawer')).to.exist;
  cy.get('#openDrawer').click();
  expect(cy.get('#avatar')).to.exist;
  cy.get('#avatar').click();
  expect(cy.get('#logout')).to.exist;
  cy.get('#logout').click();
};

Viel Code für einen kleinen Test. Daher werde ich nun den obigen Codeteil noch etwas genauer beschreiben:

before

Alles was dort definiert wurde, wird vor der ersten Testausführung einmalig erledigt. In diesem Fall, wird der Nutzer mit einen Signup angelegt.

beforeEach

Diese Methode wird vor jeden Test ausgeführt. Als erstes wird der Speicher mit clearStorage gelöscht und als nächstes wird die Login Seite aufgerufen.

afterEach

Diese Methode wird nach jeden Test ausgeführt. Dort wird abermals der Speicher gelöscht.

Test

In meinen Codebeispiel existiert nur ein einziger Test. Dieser beschreibt den Login und den Logout eines Nutzers.

Generelle Helfermethoden

Alles was danach kommt sind sogenannte Helfermethoden. Dort wird zum Beispiel beschrieben, wie man zur Loginseite kommt. Diese Methoden könnten natürlich auch noch ausgelagert werden.

Einen Test im Cypress Runner ausführen lassen

Damit ich meine Tests ausführen kann, habe ich die package.json ergänzt.

{
  "name": "cypresstest",
  "version": "0.1.0",
  "description": "Cypress test",
  "license": "MIT",
  "private": true,
  "repository": {},
  "homepage": "",
  "bugs": {},
  "author": {},
  "keywords": [],
  "engines": {
    "node": ">=9.0.0",
    "npm": ">=5.0.0",
    "yarn": ">=1.0.0"
  },
  "main": "main.js",
  "scripts": {
    "start-web-test": "webpack-dev-server -d --hot --host 127.0.0.1 --config=./webpack.web.test.config.js --mode development",
    "start-web-test-ci": "webpack-dev-server --host 127.0.0.1 --config=./webpack.web.test.config.js --mode development --quiet --inline=false --hot=false --watch-poll 6000000",
    "cy:open": "cypress open",
    "cy:run": "cypress run",
    "start-server": "npm run start-web-test",
    "start-server-ci": "npm run start-web-test-ci",
    "ci": "start-server-and-test start-server-ci http://localhost:8080 cy:run",
    "ci:open": "start-server-and-test start-server http://localhost:8080 cy:open",
  },
  "dependencies": {
    "@material-ui/core": "^4.1.1",
    "@material-ui/icons": "^4.2.0",
    "@material-ui/lab": "^4.0.0-alpha.16",
    "@material-ui/styles": "^4.1.1",
    "react": "^16.8.6",
    "react-dom": "^16.6.0",
    "react-router": "^5.0.1",
    "react-router-dom": "^5.0.1"
  },
  "devDependencies": {
    "cypress": "^3.3.1",
    "start-server-and-test": "^1.9.1",
    "webpack": "^4.34.0",
    "webpack-cli": "^3.3.4",
    "webpack-dev-server": "^3.7.1"
  }
}
  • start-web-test - Webpack Dev Server mit Hot Reload starten
  • start-web-test-ci - Webpack Dev Server ohne Hot Reload starten
  • cy:open - Cypress Runner starten
  • cy:run - Cypress im Headless Modus starten
  • start-server - start-web-test ausführen
  • start-server-ci - start-web-test-ci ausführen
  • ci - start-server-ci ausführen und anschließend cy:run durchführen
  • ci:open - start-server ausführen und anschließend cy:open durchführen

Damit das ANSCHLIEßEND bei den letzten Tasks funktioniert, muss das NPM Modul start-server-and-test installiert werden. Dadurch wird bis auf die Ausführung des Webpack Dev Servers gewartet und erst anschließend werden die Tests ausgeführt.

Nun kann ich npm run ci:open ausführen und der Webpack Dev Server wird gestartet. Anschließend wird der Cypress Runner geöffnet.

Cypress Runner

Cypress und Gitlab CI

Nun komme ich zum letzten Teil des Beitrags: Das automatische Ausführen der Tests - die Continuous Integration.

Diese Integration wird in der .gitlab-ci.yml Datei beschrieben. Damit Cypress und Node Module nicht immer installiert werden müssen, kann ein Cache definiert werden.

image: node:latest

variables:
  npm_config_cache: "$CI_PROJECT_DIR/.npm"
  CYPRESS_CACHE_FOLDER: "$CI_PROJECT_DIR/cache/Cypress"

cache:
  paths:
    - .npm
    - cache/Cypress
    - main/node_modules/

stages:
  - cypress-test

cypress-test:
  stage: cypress-test
  image: cypress/base:10
  retry: 1
  script:
    - mkdir -p ./main/cypress/videos
    - mkdir -p ./main/cypress/screenshots
    - touch ./main/cypress/videos/readme.md
    - touch ./main/cypress/screenshots/readme.md
    - cd main && rm -rf node_modules
    - npm i
    - npm run ci
  artifacts:
    when: always
    paths:
      - ./main/cypress/videos/*
      - ./main/cypress/screenshots/*
    expire_in: 7 day

Sofern ein Test fehlschlägt, wird automatisch ein Video erstellt und als Artefakt im Gitlab gespeichert. Damit das nur bei fehlgeschlagenen Tests passiert, muss die cypress.json angepasst werden:

{
  "videoUploadOnPasses": false
}

Und wenn man alles an Gitlab gesendet hat, werden die Tests automatisch ausgeführt.

Damit habe ich nun automatische UI Tests mit Cypress gestaltet und in Gitlab hinterlegt.

via GIPHY

Tags