zurück zum Artikel

End-to-End-Tests mit Protractor

Benjamin Schmid

GUI-Tests sind bei der Entwicklung qualitativ hochwertiger Webanwendungen eine gute Ergänzung zu Unit-Tests. Mit Protractor stellt Google ein speziell auf das AngularJS-Webframework abgestimmtes Werkzeug bereit.

End-to-End-Tests mit Protractor

GUI-Tests sind bei der Entwicklung qualitativ hochwertiger Webanwendungen eine gute Ergänzung zu Unit-Tests. Mit Protractor stellt Google ein speziell auf das AngularJS-Webframework abgestimmtes Werkzeug bereit.

AngularJS macht es vor: Moderne Webanwendungen laufen inzwischen vollständig im Browser ab, laden
dank Single-Page-Architektur Inhalte im Hintergrund statt über neue Seiten und nutzen HTML5, um sich auf dem Desktop und mobilen Endgeräten zu präsentieren. Wenn es ums Testen geht, stellt Googles Framework dem Entwickler mit modularer Strukturierung über Services, Dependency Injection und Mocks für Server-Zugriffe bereits viele Hilfsmittel für solide und effiziente Unit-Tests [1] zur Seite.

Oberflächentests sind eine gute Ergänzung, denn mit ihnen lässt sich die Anwendung aus der Sicht des Anwenders überprüfen. Idealerweise führt man sie automatisiert aus und nutzt echte Browser und Endgeräte. Als De-facto-Standard hat sich zu dem Zweck Selenium einen Namen gemacht. Das Tool simuliert über eine API die Aktivitäten eines Nutzers ferngesteuert auf allen relevanten Browsern.

In der Praxis kommt Selenium jedoch selten direkt zum Einsatz, denn das Erstellen stabiler GUI-Tests stellt sich als mühsame und fehlerträchtige Angelegenheit dar. Single-Page-Anwendungen bringen eine zusätzliche Schwierigkeitsstufe mit: Durch das fehlende Neuladen der Seite entfällt ein wichtiges Signal dafür, dass die ausgelöste Aktivität abgeschlossen ist und sich der Testlauf fortsetzen lässt. Um zu einer stabil funktionierenden Testlösung zu kommen, war es daher lange Zeit Standard, eigene Abstraktionsschichten zu bauen.

Auch Protractor setzt auf der bewährten WebDriver-API [2] von Selenium auf und verpackt sie in das BDD-Testframeworks (Behaviour Driven Development) Jasmine [3]. Derzeit noch experimentelle Unterstützung gibt es zudem für die beiden anderen BDD-Frameworks Mocha [4] und Cucumber. Als Schwesterprojekt von AngularJS hat Protractor fundiertes Wissen über Internas und bietet daher einen schnellen Einstieg in die Entwicklung praxisnaher Oberflächentests.

So kann der sonst übliche Code für zusätzliche Wartezeiten und Aktivitätsprüfungen entfallen. Protractor erkennt automatisch den Moment, an dem AngularJS alle Aufgaben zum Abschluss gebracht hat und die Seite vollständig synchronisiert ist. Angular-spezifische Locator-Strategien vereinfachen eine weitere Kernaufgabe: das Identifizieren und Wiederfinden einzelner GUI-Elemente auf der Website. Trotz derartiger Komfortfunktionen hat man aber stets vollen Zugriff auf die WebDriver-API und ihre Freiheiten. Dadurch ist ein Einsatz von Protractor auch in Umgebungen denkbar, die nicht mit AngularJS arbeiten.

Bevor man Protractor [5] nutzen kann, ist das Tool mit dem Befehl npm install -g protractor über den Node.js-Paketmanager zu installieren. Im Anschluss lädt man via webdriver-manager update noch die plattformspezifischen Browser-Anbindungen nach. Nun lässt sich der Selenium-Server über webdriver-manager start starten. Damit das aber gelingt, müssen zudem Java und die gewünschten Zielbrowser installiert sein.

Eine Konfigurationsdatei teilt Protractor nun mit, wo der Selenium-Server und die Tests zu finden und welche Browser zu verwenden sind. Im vorliegenden Beispiel sind das Chrome und Firefox:

// protractor-conf.js
exports.config = {
seleniumAddress: 'http://localhost:4444/wd/hub',
specs: ['*.e2e.js'],
multiCapabilities: [
{ browserName: 'firefox' },
{ browserName: 'chrome' }
]
}

Der erste einfache Testfall soll eine Taschenrechner-Beispielanwendung testen. Mit protractor protractor-conf.js läßt sich der Test nun direkt mit Protractor ausführen.

// webcalculator.e2e.js
describe('Web Calculator', function() {
it('should have a page title & Go! button', function() {
browser.get('https://juliemr.github.io/protractor-demo/');

expect(browser.getTitle()).toEqual('Super Calculator');
expect(element(by.buttonText('Go!')).isDisplayed()).toBeTruthy();
});
});

Nutzer von Jasmine oder Mocha werden im ersten Beispiel viele aus Unit-Tests bekannte Bestandteile wiederfinden. Dagegen sind browser, element und by globale Variablen von Protractor. Über browser ist die WebDriver-Instanz zum Navigieren des Browsers und zum Abrufen von Informationen über Seiten als Ganzes verfügbar. Das Symbol by stellt eine Sammlung unterschiedlicher Strategien zum Auffinden der HTML-Elemente bereit. Diese Locator-Instanzen lassen sich über element() beziehungsweise element.all() in einzelne oder mehrere ElementFinder-Objekte überführen, die dann in den Tests als Stellvertreter zum Steuern beziehungsweise Auslesen der adressierten GUI-Elemente dienen.

Die Locator-Factory by bietet einige hilfreiche Standardherangehensweisen, um GUI-Elemente in Angular-Anwendungen einfach anhand des Modellnamens oder des Binding-Ausdrucks aus der Implementierung anzusprechen. Andere Methoden erlauben auch die allgemeinere Suche über Textfragmente und CSS-Eigenschaften. Wem das noch nicht genügt, der kann jederzeit auch eigene Locator-Strategien entwerfen und an der Factory registrieren.

Zudem lassen sich die über die Locator erzeugten ElementFinder-Instanzen auch noch verketten, um GUI-Elemente durch umschließende Bestandteile leichter einzugrenzen:

element(by.css("form")).element(by.model("first"))

Nach dem Finden und Adressieren der Elemente geht es daran, sie auch in Tests zu nutzen. Dazu stehen auf allen ElementFinder-Instanzen die aus Selenium bekannten Methoden wie click() zum Simulieren von Mauseingaben oder sendKeys() für Tastatureingaben bereit. Um die Reaktion der GUI zu überprüfen, lässt sich die im Browser dargestellte Situation über eine Vielzahl an Möglichkeiten abfragen. Neben Textinhalten kann man an der Stelle auch andere visuelle Eigenschaften wie Sichtbarkeit, Position oder CSS-Attribute der Elemente abfragen.

var tab = protractor.Key.TAB;
element(by.model('first')).sendKeys("6", tab, tab, "7");
element(by.cssContainingText('option', '*')).click();
element(by.partialButtonText('Go')).click();

expect(element(by.binding('{{latest}}')).getText()).toEqual('42');

Gerade für die ersten Gehversuche ist das mitgelieferte Node-Skript elementexplorer.js hilfreich. Mit seiner Hilfe lässt sich eine Protractor-Sitzung in der Kommandozeile zusammen mit einem Chrome-Browserfenster starten. Nun kann man seine Protractor-Befehle direkt interaktiv in der Konsole ausprobieren, verfeinern und ihre Funktion direkt im Browser verfolgen. Weitere Hinweise, wie man Protractor auch über die IDE, den Browser oder Screenshots debuggen kann, liefert die offizielle Protractor-Dokumentation [6].

Bislang wurden nur einzelne, eindeutig addressierte Elemente betrachtet. Natürlich hat man es in der Praxis regelmässig mit mehreren Elementen oder einem bestimmten Eintrag daraus zu tun.

Listen werden in AngularJS in der Regel über die Direktive ng-repeat erzeugt. Mit der Strategie by.repeater() bietet Protractor einen Weg, sie als Ganzes oder bestimmte Zeilen und Spalten daraus zu adressieren. Alternativ nutzt man einfach die regulären Locator so, dass sie auf mehrere Elemente passen, und wandelt sie über element.all() in eine Liste um. Mit each(), map(), filter() und reduce() bieten die Listen zudem weitere, aus den JavaScript-Arrays bekannte Komfortfunktionen.

var locator = by.repeater('result in memory').column('result.value');
expect(element.all(locator).get(0).getText()).toBe('4');
expect(element(locator.row(1)).getText()).toBe('9');

var inputs = element.all(by.css("input"));
expect(inputs.count()).toEqual(2)
inputs.first().sendKeys("1");
inputs.get(1).sendKeys("2");

Eine wichtige Eigenschaft ist bislang in allen Beispielen noch etwas versteckt geblieben: Alle Protractor-Tests sind vollständig asynchron. Somit werden alle Aufrufe zum Steuern oder Auslesen von Browser-Elementen nicht sofort ausgeführt, sondern zuerst nur in eine Warteschlange eingereiht. Der Aufruf von browser.getTitle() etwa liefert daher nicht den tatsächlichen Titel, sondern nur ein JavaScript-Promise für das Ergebnis als Platzhalter. Protractor erweitert Jasmine nun aber so, dass auch alle betroffenen expect()-Aufrufe ihre Prüfbedingungen nur einreihen. Am Ende jedes Tests wird dann zuerst darauf gewartet, dass die übergebenen Promises aufgelöst sind, bevor die Prüfung der Expectations beginnt. Wie die Beispiele zeigen, geschieht das in der Regel vollständig transparent hinter der Jasmine-API. In Fällen, in denen man mit den bereitgestellten Mitteln aber nicht zurechtkommt, muss man darauf achten, seine Prüfungen immer in den then()-Block der zurückgegebenen Promises zu verlagern.

Mit dem vorgestellten Rüstzeug und etwas Einarbeitung in die interaktive Protractor-Konsole stellen sich die ersten eigenen Erfolge recht schnell ein. Eine klare Struktur kann dabei helfen, dass die Tests auch den realen Projekteinsatz bestehen und dauerhaft wartbar bleiben.

Außer der Aufteilung in unterschiedliche Testsuites ist es in der Praxis häufig sinnvoll, den Code für die technisch geprägten Selektoren und Elemente in sogenannte Page Objects auszulagern und damit von der eigentliche Testlogik getrennt zu halten. So bleibt der eigentliche Testcode gut lesbar und Änderungen an den HTML-Vorlagen beeinflussen nur die Datei mit den Page Objects.

var CalcPageObjects = require('./calculator.po.js');

it('should allow to calculate', function() {
calc = new CalcPageObjects();
calc.firstValue.sendKeys("6");
calc.multiplyOption.click();
calc.secondValue.sendKeys("7");
calc.goButton.click();

expect(calc.resultLabel.getText()).toEqual('42');
});

Um generell aber Seiteneffekte bei Änderungen der Oberfläche zu reduzieren, bevorzugt man bei der Gestaltung der Selektoren vorzugsweise die vorgestellten AngularJS-spezifischen Varianten statt allgemeiner CSS-Ausrücke oder IDs.

Ihrem Namen folgend prüfen Ende-zu-Ende-Tests stets das Gesamtsystem. Dazu setzen sie typischerweise bestimmte Ausgangsdaten voraus, ändern aber im Rahmen ihrer Ausführung auch Inhalte. Das unterscheidet sie deutlich von Unit-Tests, die gemäß dem FIRST Prinzip [7] isoliert, schnell und einzeln wiederholbar bleiben sollten.

Mit der richtigen Strategie gelingt das Schreiben von Protractor-Tests nahezu genauso komfortabel wie das von Unit-Tests. Das gilt inbesondere, da während der Entwicklungszeit die Modellnamen bereits griffbereit sind und sich direkt in die Locator-Strategien einsetzen lassen. Es liegt also nahe, zur Ergänzung einzelne Oberflächentests schnell, isoliert und regressionsfähig zu gestalten. Damit das im Alltag gelingt, sind lediglich zwei Dinge nötig:

Insgesamt stellt dieser alternative Ansatz, die Tests nur auf die Oberflächenlogik zu begrenzen, eine attraktive Ergänzung zu den ebenfalls sinnvollen, aber deutlich aufwendigeren und fragileren Ende-zu-Ende-Funktiontests dar.

Mit Protractor hat Google ein spannendes Werkzeug für einfache und stabile Oberflächentests aus der Taufe gehoben. Insbesondere die AngularJS-spezifischen Erweiterungen erleichtern die Arbeit mit dem zugrunde liegenden Selenium-WebDriver-Framework erheblich. Schon nach wenigen Minuten stellen sich vielversprechende Erfolge ein. Und auch nach längerem Einsatz überrascht Protractor, da die elegante API die sonst üblichen Behelfslösungen wie willkürliche Wartezeiten oder automatische Wiederholungen überflüssig macht.

Sicher ist aber auch: Komplett problemlos sind Ende-zu-Ende-Tests auch mit Protractor nicht. In der Praxis macht einem öfter die eigene Infrastruktur einen Strich durch die vollautomatische Testausführung. Und obwohl die Möglichkeiten zum Ansprechen einzelner Elemente vieles stark vereinfachen, bleibt das Schreiben von Tests auch mit dem Tool eine Engineering-Tätigkeit. Dafür aber eine, die richtig Spaß machen kann und einen echten Mehrwert mit sich bringt.

Benjamin Schmid
ist ein Anhänger sauberen Programmcodes und betreut seine Kollegen als Technology Advisor bei der eXXcellent solutions GmbH in allen technologischen und methodischen Fragestellungen.
(jul [8])


URL dieses Artikels:
https://www.heise.de/-2461535

Links in diesem Artikel:
[1] http://1253805
[2] https://code.google.com/p/selenium/wiki/WebDriverJs
[3] https://www.heise.de/hintergrund/Unit-Tests-mit-dem-JavaScript-Framework-Jasmine-1709077.html
[4] https://www.heise.de/blog/Mocha-Co-2191178.html
[5] http://protractortest.org/
[6] https://github.com/angular/protractor/blob/master/docs/debugging.md
[7] http://agileinaflash.blogspot.de/2009/02/first.html
[8] mailto:jul@heise.de