E2E-Testing mit Protractor – Teil 3: Konfiguration von Ausführung und Umgebung

Nachdem sich die ersten beiden Teile mit einem grundsätzlichen Setup sowie einer praktikablen Test-Struktur beschäftigt haben, wird es in diesem um die Möglichkeiten zur Konfiguration der Ausführung sowie der dabei genutzten Umgebung gehen.

In den bisherigen Teilen wurden die Tests immer gegen lokal laufende Browser ausgeführt, deren Verhalten man direkt beobachten konnte beziehungsweise sogar musste. Während dieser Ablauf in der lokalen Entwicklung durchaus gewünscht sein kann, wird er für automatisierte Systeme (beispielsweise im Bereich CI), insbesondere solche ohne physisches Display, zum Problem. Weiterhin wäre es mitunter hilfreich, den Test-Runner und den gerade getesteten Browser nicht auf demselben System laufen lassen zu müssen.

In diesem Teil wird es zunächst darum gehen, verschiedene Parameter für die Ausführung eines Tests setzen zu können, was letztlich auch die Voraussetzung für die beiden genannten Punkte sowie auch bspw. die Remote-Steuerung eines Browsers im Test ist. Die beiden genannten Punkte werden im Anschluss daran als praktische Beispiele behandelt.

Argumente via CLI mitgeben und nutzen

Ein wesentlicher Vorteil der Verwendung eines Task-Runners ist die vereinfachte Integration von zusätzlichen Command-Line Parametern im Vergleich zur unmittelbaren Ansteuerung von Protractor. Für Node.js gibt es diverse Bibliotheken, um diese Argumente zu Parsen – wir werden hier beispielhaft yargs verwenden. Auf umfassende Konfiguration wird dabei verzichtet, auch wenn dies natürlich möglich ist.

Zur Nutzung des Parse-Resultats muss dem Gulpfile folgendes hinzugefügt werden:

import {argv} from 'yargs';

Dieses ist nicht zu verwechseln mit process.argv! Das auf diesem Weg importierte Objekt enthält die eingelesenen Parameter sowohl in Spinal- als auch in Camel-Case Notation.

Als erstes kleines Beispiel für den Einsatz dieses Objekts soll die manuelle Konfigurierbarkeit der Basis-URL dienen. Da diese letztlich das Ziel des Tests ist, wird der zugehörige Parameter mit target benannt. Letztendlich soll es möglich sein, einen Test gegen eine bestimmte URL wie folgt starten zu können:

gulp e2e:chrome --target={your-url-here}

Zunächst wird hierfür eine Funktion benötigt, welche unter Verwendung des mitgegebenen Parameters die Ziel-URL ermitteln kann.

let determineTarget = function () {
  let target = argv.target || 'http://localhost:3333';
  if (!/^https?/.test(target)) {
    target = `http://${target}`;
  }
  return target;
};

Die o.g. Umsetzung illustriert nebenbei noch, wie die Angabe des Protokolls optional gemacht werden kann, was i.d.R. ohnehin nur für die Unterscheidung zwischen http und https notwendig wäre.

Das Ergebnis dieser Funktion muss nun noch in die für Protractor bestimmte Parameterliste eingepflegt werden:

.pipe(protractor({
  configFile: `test/e2e/configs/${browserName}.js`,
  args: [
    '--baseUrl',
    determineTarget()
  ]
}))

Ein kurzer Testlauf über bspw.

gulp e2e:chrome --target 10.2.1.65:3333

wird nun die angegebene URI ansteuern, anstelle des Defaults. Umgekehrt wird entsprechend bei Weglassen dieses Parameters der bisherige Default verwendet.

(Der o.g. Test wäre bspw. mit der Netzwerk-IP eures Rechners nachstellbar, oder mit dessen Public-IP, falls vorhanden.)

Headless testing

Headless testing beschreibt in diesem Zusammenhang die Durchführung von Tests gegen ein UI, ohne dabei ein physisches Display verwenden zu müssen. Dies ist bspw. in einer CI-Umgebung (Jenkins o.ä.) von Vorteil, welche in der Regel auf Servern ohne physisches Display laufen. Für einen solchen Test ist entsprechend eines der folgenden Werkzeuge notwendig:

  • ein virtuelles Display bzw. ein Framebuffer
  • ein Browser bzw. ein entsprechender Emulator, welcher über einen aktivierbaren Headless Mode verfügt.

Zur letztgenannten Kategorie zählen bspw. PhantomJS oder Electron. Während diese sehr gut für bspw. Unit-Tests auf Karma-Basis funktionieren und völlig ausreichen, gestaltet sich die Nutzung für E2E-Tests als problematisch, da ihr grundlegendes Verhalten von „normalen“ Browsern abweicht – teils mehr, teils weniger. E2E-Tests haben aber letztlich das Ziel, möglichst nahe an dem zu sein, womit sich Benutzer letztendlich konfrontiert sehen. Damit bleibt also nur die erstgenannte Variante.

Während es hier für Windows zumindest für beliebige Tests eher düster aussieht, gibt es für Linux und andere unixoide Systeme mehrere Möglichkeiten.

  • XVNC: Bietet sich vor allem bei Verwendung von Jenkins an – hier gibt es bspw. das XVNC-Plugin, um ein Display für die Dauer der Durchführung der E2E-Tests bereitzustellen.
  • Xvfb: Ein X-Server, welcher einen virtuellen Framebuffer verwendet. Bietet sich vor allem an, da es diverse Bindings zum Ansteuern für unterschiedliche Programmiersprachen und -umgebungen gibt. Beispiele hierfür wären solche für Node.js (was wir im Falle von Protractor gebrauchen können) oder Ruby (für Nutzer von Watir).

Unter Windows könnte man ggf. mit XNVC-Portierungen wie TigerVNC arbeiten; dieser Pfad wird hier aber nicht weiter verfolgt.

Für das hier behandelte Beispiel wird das letztgenannte Xvfb genutzt. Wichtig ist, diesen als optionale Abhängigkeit zur package.json hinzuzufügen, damit es bei nicht unterstützen Systemen zu keinen Fehlern kommt, obwohl alles abseits des Headless Mode dort nach wie vor funktioniert. 

"optionalDependencies": {
  "xvfb": "^0.2.2"
}

Der Import im Gulpfile muss entsprechend die Nicht-Präsenz von Xvfb tolerieren können. Dies lässt sich über die require-Syntax einfacher realisieren als über die sonst genutzten Imports:

let Xvfb = null;
try {
  Xvfb = require('xvfb');
} catch (error) {
  gulpUtil.log(`[${gulpUtil.colors.red("?")}] Xvfb not found, headless mode will not be available.`);
}

Die Ausgabe einer Meldung ist optional, kann aber für den nutzenden Entwickler auch als Hinweis dienen, dass die Installation nach Festlegung als Abhängigkeit evtl. nur vergessen wurde.

Im nächsten Schritt wird eine Funktion benötigt, welche den eigentlichen Aufruf der Test-Pipeline in Start und Stop einer Xvfb-Instanz wrappen kann:

let runWithHeadless = function (execFunc, done) {
  let xvfb = new Xvfb();
  xvfb.start(function (err) {
    if (err) {
      throw err;
    }
    execFunc().on('end', function () {
      gulpUtil.log('Stream ended, shutting down Xvfb...');
      xvfb.stop(function (err) {
        if (err) {
          throw err;
        }
        done();
      });
    });
  });
};

Das Callback done wird hierbei vom Gulp-Task mitgegeben, welcher damit die Verantwortung für die korrekte Benachrichtigung über den Abschluss des Tests an die Funktion weiterreicht. Dies ist notwendig, da eine asynchrone Stop-Funktion verwendet wird, welche nicht Teil des verwendeten Streams ist.

Extrahiert man nun noch die Funktion zum Aufruf der Pipeline für Protractor aus der Task-Deklaration in eine Variable executionFunction, kann die Taskdefinition reduziert werden auf:

let useHeadlessMode = Xvfb && argv.headless;
supportedBrowsers.forEach((browserName) => {
  gulp.task(`e2e:${browserName}`, ['webdriver:update'], function (done) {
    if (useHeadlessMode) {
      runWithHeadless(executionFunction.bind(null, browserName), done);
    } else {
      executionFunction(browserName).on('done', done);
    }
  });
});

Der Aufruf von

gulp e2e:chrome --headless

wird nun entsprechend einen Test durchlaufen, ohne dabei ein sichtbares Window für den Browser zu öffnen.

Hinweise:

  • Xvfb wird vor und nach dem Test einigen Output produzieren, welcher sich nicht ohne Weiteres unterdrücken lässt. Dieser kann problemlos ignoriert werden.
  • Unter Linux kommt es im Falle von Chromium und Chrome mitunter zu einer Meldung in Form eines Dialogs, dass der Browser „unerwartet beendet wurde“. Dies ist inhaltlich nicht wahr und kann ebenfalls ignoriert werden.

Remote testing

Remote testing beschreibt Testläufe unter Nutzung eines nicht-lokalen Zielbrowser, bspw. des Internet Explorer innerhalb einer Windows-VM ausgehend von einem Linux-Host. Hierfür muss eine Instanz des Selenium-Servers auf dem Zielsystem laufen und Protractor diesen anstelle der lokalen Variante ansteuern.

Setup Zielumgebung

Zunächst müssen die folgenden Downloads und Installationen auf dem Zielsystem durchgeführt werden (u.g. sind auf dem Stand 2. Dezember 2015, da sie in dieser Form auch noch im Beispiel-Repository verwendet werden):

Entpackt nun die ZIP-Dateien in einen beliebigen Ordner und verschiebt auch die JAR-Datei des Server dorthin. Sollte Safari unter MacOSX das Testziel sein, muss darin die o.g. Erweiterung installiert werden. Startet dann den Server mit einem der folgenden Skripte.

Achtung: Wer via Remote-Zugriff Safari bzw. Internet Explorer testen möchte, sollte zunächst die speziellen Abschnitte am Ende dieses Kapitels beachten. Die u.g. Variante wird nur für Chrome und Firefox ohne weitere Anpassungen laufen.

Windows:

@echo Starting Selenium Standalone Server
java -jar selenium-server-standalone-2.48.2.jar -port 4444 -Dwebdriver.ie.driver=IEDriverServer.exe -Dwebdriver.chrome.driver=chromedriver.exe
@pause

Linux / MacOSX:

#!/usr/bin/env bash
echo "Starting Selenium Standalone Server"
java -jar selenium-server-standalone-2.48.2.jar -port 4444 -Dwebdriver.chrome.driver=chromedriver

Die Skripte starten den Selenium-Server auf Port 4444 (kann nach Bedarf angepasst werden) und setzen die Pfade für die spezifischen Treiber (ebenfalls anpassungsfähig, sofern die Treiber an einem anderen Ort liegen sollen).

Nach einem kurzen Augenblick sollte selbiger laufen und auf Zugriffe warten.

Setup Test-Runner

Das Setup hier ist aufgrund der Tatsache, dass Protractor als Child-Prozess läuft, etwas umständlicher:

  • Via CLI muss ein Argument entgegengenommen werden, welches den Test als Remote markiert und dabei die Adresse und Port des Hosts mit dem laufenden Selenium-Servers mitgibt.
  • Dieses Argument muss an den zu spawnenden Protractor-Prozess weitergegeben werden.
  • Je nachdem, ob dieses Argument gesetzt wurde, muss in der Konfiguration ein zusätzliches Feld seleniumAddress als Ziel für den Test-Runner hinzugefügt werden. Dieser Parameter wird höher gewichtet als seleniumServerJar, welches gesetzt werden kann, um auf ein bestimmtes lokales Verzeichnis mit dem Selenium-Server zu verweisen.

Zunächst legen wir hierfür einen Builder an, welcher die Parameter des Protractor-Aufrufs auf Basis der CLI-Argumente zusammenstellt. Hier wird angenommen, dass der Parameter zum triggern eines Remote-Tests mit remote benannt wurde.

let determineTarget = function (argv) {
  let target = argv.target || 'http://localhost:3333';
  if (!/^https?/.test(target)) {
    target = `http://${target}`;
  }
  return target;
};
 
export let protractorCfgBuilder = function (browserName, argv) {
  let remote = argv.remote || false,
    baseArgs = [
      "--baseUrl",
      determineTarget(argv)
    ];
 
  if (remote) {
    if (!/^https?/.test(remote)) {
      remote = `http://${remote}`;
    }
    baseArgs.push('--remote', remote);
  }
 
  return {
    configFile: `test/e2e/configs/${browserName}.js`,
    args: baseArgs
  };
};

Die executionFunction im Gulpfile wird entsprechend angepasst:

import {protractorCfgBuilder} from './test/e2e/configs/builder';
// ...
let cfg = protractorCfgBuilder(browserName, argv);
return gulp.src('test/e2e/specs/**/*.spec.js')
  .pipe(protractor(cfg))
  .on('error', (e) => {
    throw e;
  });

Zusätzlich muss noch die Unterstützung für den Headless Mode deaktiviert werden, sofern ein Remote-Test angesteuert wird – der Testrunner an sich braucht ohnehin nur eine Konsole, und auf die ausgeführte Instanz kann man von Remote keinen derartigen Einfluss nehmen.

let useHeadlessMode = Xvfb && argv.headless && !argv.remote;

Im nächsten Schritt muss die von Protractor über die browser-spezifische Konfiguration aufgerufene baseConfig angepasst werden, um den weitergeleiteten Parameter auswerten zu können:

var defaultTimeout = 60000,
  argv = require('yargs').argv;
 
var generator = function () {
  var options = {
    // Set jasmine2 to be used as testing framework.
    framework: 'jasmine2',
    getPageTimeout: defaultTimeout,
    allScriptsTimeout: defaultTimeout,
    defaultTimeoutInterval: defaultTimeout,
    // Just to get a more colored highlighting in the spec results.
    jasmineNodeOpts: {
      showColors: true
    },
    onPrepare: () => {
      // E.g., we might add a better structured command line reporter.
      var SpecReporter = require('jasmine-spec-reporter');
      jasmine.getEnv().addReporter(new SpecReporter());
 
      require('babel-core/register');
    }
  };
 
  if (argv.remote) {
    options.seleniumAddress = argv.remote + '/wd/hub';
  }
 
  return options;
};
 
module.exports = generator();

Die an die Selenium-Adresse angehängte Sub-URL /wd/hub ist immer identisch und kann entsprechend direkt im Code stehen. Das Setup ist damit komplett.

Nun benötigt ihr die IP oder den Namen des Hosts, auf dem der Selenium-Server läuft, sowie dasselbe noch für den Host des Anwendungsservers. In meinem Fall wären das bspw.:

  • Selenium-Host: 10.2.1.108
  • Anwendungs-Host: 10.2.1.65

Als Port des Selenium-Server stellt das o.g. Startskript des Servers 4444 ein. Wenn der Anwendungsserver wie im Beispiel auf Port 3333 läuft, kann nun ein Test via

gulp e2e:chrome --remote=10.2.1.108:4444 --target=10.2.1.65:3333

gestartet werden.

Workaround Firefox

Wer hier – wie ich – Probleme mit Firefox hatte, bei welchen die Functionality Suite fehlschlug, weil das Element mit der ID todo-page nicht mehr gefunden werden konnte, kann wie folgt vorgehen. Das Problem wird verursacht durch ein Scrollverhalten des Browsers bei der Interaktion mit dem Datepicker. In anderen Browsern konnte selbiges noch nicht nachgestellt werden.

In der baseConfig.js die Funktion onPrepare wie folgt ergänzen:

return browser.getCapabilities().then(function(caps) {
  global.browserCapabilities = caps;
});

Dadurch werden die Browser-Eigenschaften im globalen Bereich hinterlegt.

Dann in der todo.spec.js am Ende der beforeAll-Funktion folgendes einfügen:

if (/firefox/.test(global.browserCapabilities.get('browserName'))) {
  browser.driver.manage().window().setSize(1024, 2048);
}

Dadurch wird das Browserfenster vor allem in der Höhe vergrößert, sofern der Browser als Firefox identifiziert wurde. Dadurch findet das o.g. Scrolling nicht mehr statt.

Spezielles für den Internet Explorer

Zunächst ein Wort der Warnung – sowohl für lokale als auch für Remote-Tests ist es notwendig, diverse Sicherheitseinstellungen des Internet Explorers herabzusetzen bzw. zu deaktivieren, da dieser die Kontrolle via WebDriver sehr oft fälschlicherweise (da hier nicht ungewollt) blockiert. Die genaue Auflistung der zu ändernden Einstellungen mit Screenshots ist hier unter Schritt 4 zu finden. Im Anschluss muss die wie o.g. heruntergeladene IEDriverServer.exe einmalig manuell selbst gestartet werden, um die Erlaubnis für den Netzwerkzugriff zu setzen – siehe auch unter dem genannten Link unter Schritt 6.

Dann wird eine entsprechende Konfiguration für den Internet Explorer benötigt (hier als Beispiel für Version 11):

var baseConfig = require('./baseConfig'),
  _ = require('lodash/object');
 
var specificConfig = {
  capabilities: {
    browserName: 'internet explorer',
    version: '11',
    ignoreProtectedModeSettings: true
  }
};
 
module.exports.config = _.merge(baseConfig, specificConfig);

Zusätzlich muss der Internet Explorer als unterstützter Browser in die Liste aufgenommen werden:

let supportedBrowsers = ['firefox', 'chrome', 'ie11'];

Für den Remote-Test kann analog zum obigen Beispiel nach dem Start des Selenium-Servers der Testlauf via

gulp e2e:ie11 --remote=10.2.1.108:4444 --target=10.2.1.65:3333

gestartet werden. Windows-lokale Tests können entsprechend auf die genannten Parameter verzichten. Falls andere Versionen des Internet Explorers testbar gemacht werden sollen, können entsprechend separate Konfigurationen hinterlegt werden.

Ein weiteres Wort der Warnung – es ist durchaus möglich, dass bei der Remote-Variante die Tests mit einem Timeout fehlschlagen, weil die Eingabe des Tasknamens zu lange dauert. Dies lässt sich leider nicht zuverlässig beheben. Mitunter hilft ein Wechsel auf eine andere Version des Selenium-Servers oder des IE-Drivers. Ob und in welchem Ausmaß das Problem auftritt, hängt auch mit der jeweils getesteten Version des Internet Explorers zusammen – in Version 11 tritt dies bspw. häufiger auf als in Version 9. Zusätzlich spielen hier Dinge wie installierte Windows-Updates eine Rolle, die z.T. Wechselwirkungen auf das genaue Verhalten haben. Siehe hierzu auch Teil 6 dieser Einführung.

Spezielles für Safari (OSX)

Zunächst muss im Ziel-Safari die eingangs referenzierte Extension installiert werden.

Dann wird eine entsprechende Konfiguration für Safari benötigt:

var baseConfig = require('./baseConfig'),
  _ = require('lodash/object');
 
var specificConfig = {
  capabilities: {
    browserName: 'safari',
    ignoreProtectedModeSettings: true
  }
};
 
module.exports.config = _.merge(baseConfig, specificConfig);

Bitte hier auf keinen Fall eine explizite Version anzugeben. In den Testläufen ergab sich immer wieder, dass die Versionsnummer, obwohl korrekt, als „Unknown“ detektiert und der Test daher abgebrochen wurde (getestet mit Safari 7+).

Zusätzlich muss die Liste unterstützter Browser erweitert werden:

let supportedBrowsers = ['firefox', 'chrome', 'ie11', 'safari'];

Für den Remote-Test kann analog zum obigen Beispiel nach dem Start des Selenium-Servers der Testlauf via

gulp e2e:safari --remote=10.2.1.108:4444 --target=10.2.1.65:3333

gestartet werden. Bei lokalen Tests können entsprechend die genannten Parameter weggelassen werden.

Ein Wort der Warnung – vor allem Safari (andere Browser weniger bis gar nicht) wird unter OSX durch den Selenium-Server im Hintergrund gestartet, sofern noch ein anderes Fenster offen ist. Dies führt dazu, dass der Browser keinen Fokus hat – was wiederum diverse Tests fehlschlagen lassen kann. Hier muss auf die Fokussierung geachtet und ggf. manuell eingegriffen werden. Weiterhin wird der Browser nach dem Test nicht immer vollständig geschlossen, sondern nur das jeweils in Anspruch genommene Tab. Ohne einen frischen Start kommt es bei aufeinander folgenden Testläufen ebenso zu diversen Problemen.

 

Mit den hier gezeigten Konfigurationsmöglichkeiten können E2E-Tests u.a. auch auf CI-Systemen ohne große Probleme eingerichtet und in die bestehenden Prozesse integriert werden. Damit sind wir gewappnet, um uns im nächsten Teil mit nicht mehr ganz so einfachen Interaktionen wie z.B. mit Select-Boxen, Dropdown-Menüs oder Alerts oder auch schlichtweg Maus- oder Touch-Bewegungen zu beschäftigen. Die erreichten Integrationsmöglichkeiten soll sich ja letztlich nicht nur für triviale Oberflächen lohnen.

Teilen Sie diesen Beitrag

Das könnte dich auch interessieren …

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert