Category Archives: typescript

Typesafe Testing – Protractor, Grunt und Typescript

TL;DR

Protractor ist das Integration-Test Tool für AngularJS Applikationen. Protractor ist das Selenium für AngularJS. Zusammen mit Typescript können extrem effektiv und typsicher Integrationstests geschrieben werden. Ein klein bisschen Vorarbeit ist zu tun – die Integration mit Grunt und vor allem die vollautomatische Integration mit Definitely Typed. Ist jedoch einmal alles konfiguriert und funktioniert gehts richtig zur Sache.

Motivation

Mit TypeScript hat Microsoft eine Sprache geschaffen, die für erfahrene Programmierer beinahe schon intuitiv verwendbar ist und nach JavaScript transpiliert und übersetzt wird.

Typescript versteckt viel vom unnötigen Balast, den Javascript aufgrund seines Erbes auf den Schultern trägt. Und – mit Typescript machen wir bereits heute einen großen Schritt in Richtung ECMAscript 6. Typescript integriert sich wunderbar in jedes Javascript-Framework – jeder Javascript-Code ist grundsätzlich gültiger TypeScript-Code.

Dies ist aber kein Typescript-Tutorial, es gibt es dutzende (hunderte…) im Web, beispielsweise hier. In diesem Blogeintrag geht es um das Zusammenspiel zwischen AngularJS, Protractor und Typescript. Protractor ist das End-2-End-Testing-Framework für AngularJS. Quasi das Selenium der Typescript-Welt. Protractor basiert  intern auf Selenium.

Wenn wir von Javascript-Frameworks sprechen ist AngularJS meist nicht weit. AngularJS macht Spaß, ich denke, wir sind uns einig. Im Vergleich zu noch ein paar Jahren ist es “kinderleicht”, Funktionalität in den Browser zu bringen. Diese Funktionalität muss getestet werden.

Wie in jedem Softwareprojekt gibt es (mindestens..) zwei Arten von Tests, die Sinn machen und die implementiert werden sollten.

Unit-Tests sind ohne Frage der wichtigste Teil. AngularJS lässt sich problemlos testen mit beispielsweise Karma und Jasmine.Unit-Tests sind für uns Entwickler. Mit Unit-Tests  erhalten wir sofort Feedback, ob nach Code-Änderungen die Software immer noch den Requirements entspricht. Auf gut deutsch – haben wir jetzt was kaputt gemacht oder nicht?

Integration-Tests (oder UI-Tests..) haben einen ganz anderen Fokus. Wir möchten sicherstellen, dass die Anwendung immer noch mit anderen Systemen zusammen funktioniert. Ein schönes Beispiel ist die Integration mit einem REST-Service. Innerhalb eines Unit-Tests können wir nur statisch überprüfen ob unsere Anwendung immer noch mit einem JSON-Schema konform ist.

Wir könnten beispielsweise eine Response des Services als JSON speichern und einen Unit-Test schreiben, der überprüft, ob wir mit dieser Response umgehen können. Das schützt uns aber nicht davor, bei Änderungen des Datenformats plötzlich vor einem Problem zu stehen. Das schlimmste ist, wir kriegen es erst in Produktion mit, wenn die ersten Tweets eintreffen und die Kunden sich über unseren kaputte Anwendung aufregen.

Als professionelle Softwareentwickler ist diese Situation nicht tragbar. Genau aus diesem Grund schreiben wir zusätzlich zu den Unit-Tests einige Integration-Tests. Wichtig hierbei ist, dass wir die Struktur der Test-Pyramide einhalten.

Ein Protractor-Test ist kein Unit-Test und kann auch keinen Unit-Test ersetzen.
Lesen Sie den letzten Satz laut 5 mal.

Ein Protractor-Test läuft gegen die “echte” und deployte Anwendung. Wir schicken echte Requests gegen das Backend ab und können sicherstellen, dass die Anwendung nach wie vor reagiert. Sollte etwas nicht funktionieren kriegen wir das proaktiv mit, nicht erst in Produktion.

Der Use Case

Der Source-Code für unsere Anwendung findet sich hier. Die Anwendung ist ein Fork vom offiziellen AngularJS-Protractor Tutorial. Wir arbeiten allerdings direkt auf der deployten Anwendung.

Projekt Setup

Klonen Sie am besten das Repository hier. Wechseln Sie nach dem Clone auf den Branch initial-setup.

Wir haben hier ein leeres NPM-Projekt Template.

Zunächst einmal installieren wir Grunt. Grunt ist ein Javascript-Task-Runner, der uns dabei unterstützt, so viel wie möglich zu automatisieren.

npm install -g grunt grunt-cli

Sobald Npm alle notwendigen Abhängigkeiten geladen hat sind wir bereit für das initiale Grunt-Setup. Wir erzeugen ein leeres Gruntfile.js.

module.exports = function(grunt) {
    grunt.initConfig({
    });

    grunt.registerTask('default', []);
}

Grunt Typescript Support

Im nächsten Schritt verheiraten wir Grunt und Typscript und installieren das grunt-ts modul.

npm install --save grunt-ts

Anschließend teilen wir Grunt mit, dass sich unsere Typescript-Sourcen künftig im Ordner “src/script” befinden werden. Grunt-TS soll alle Dateien mit der Endung *.ts automatisch transpilieren.

 grunt.initConfig({
        ts: {
            default: {
                src: ["src/script/**/*.ts"]
            }
        }
    });

Der erste Code

Im Ordner “src/script” erzeugen wir eine neue Datei “sample.spec.ts” und wir erzeugen hier nichts weiter als eine Typescript-Module-Definition und eine leere Klasse Test.

module protractor.testing {   
  export class Test {}
}

Anschließend führen wir grunt aus und die Datei wird sofort nach Javascript transpiliert und im gleichen Ordner abgelegt.

var protractor;
(function (protractor) {
    var testing;
    (function (testing) {
        var Test = (function () {
            function Test() {
            }
            return Test;
        })();
        testing.Test = Test;
    })(testing = protractor.testing || (protractor.testing = {}));
})(protractor || (protractor = {}));

Das Modul wird in separate Javascript-Namespaces übersetzt.

Wie schreiben wir jetzt aber unseren ersten Test?

Protractor

Zunächst brauchen wir Protractor selbst.

npm install --save protractor

Protractor bringt alles mit, was es braucht um zu arbeiten. U.a. den Selenium-Server und die WebDriver um mit den gängigsten Browsern zu kommunizieren.

Im ersten Schritt kopieren wir uns die Beispielkonfiguration aus dem Protractor-Verzeichnis.

cp node_modules/protractor/example/conf.js protractor.conf.js

Die Datei protractor.conf.js sieht aktuell so aus

exports.config = {
  directConnect: true,

  // Capabilities to be passed to the webdriver instance.
  capabilities: {
    'browserName': 'chrome'
  },

  // Spec patterns are relative to the current working directly when
  // protractor is called.
  specs: ['example_spec.js'],

  // Options to be passed to Jasmine-node.
  jasmineNodeOpts: {
    showColors: true,
    defaultTimeoutInterval: 30000
  }
};

Über das Attribute directConnect legen wir fest, ob Protractor direkt über den WebDriver mit dem Browser kommuniziert oder ob ein Selenium-Server dazwischen sein soll.  Ich gehe davon aus, dass für die meisten Fälle eine direkte Kommunikation mit dem Browser besser ist, wir setzen das Attribute also auf true und müssen deswegen nicht zusätzlich noch einen Selenium-Server starten.

Eine generelle Übersicht über die Art und Weise wie protractor mit dem Browser kommuniziert findet sich hier

Über die capabilities steuern wir, wie und mit welchem Browser kommuniziert wird. Wir können hier auch mehrere Browser konfigurieren und so Multi-Browser-Tests machen.

Über die Konfiguration specs steuern wir, wo Protractor unsere Testfälle findet. Das ändern wir folgendermaßen:

specs: ['src/script/**/*.js']

Warum die Änderung *.js und nicht *.ts? Protractor lädt die hier konfigurierten Dateien im Browser. Ein Browser versteht (üblicherweise) kein Typescript. Wir konfigurieren hier also die bereits transpilierten Sourcen.

Ein erster Test

Definieren wir einen ersten einfachen Test-Case nur um prüfen, ob Protractor funktioniert.

#sample.spec.ts
module protractor.testing {
    export class Test {
        protractorExample() {
            browser.get("http://juliemr.github.io/protractor-demo/")
        }
    }
}

Lassen wir die Sourcen nach Javascript übersetzen (grunt ts) beschwert sich Typescript zurecht.

sample.spec.ts(4,13): error TS2304: Cannot find name 'browser'.

Typescript kennt Protractor nicht, deswegen kennt es auch das Browser-Objekt nicht. Die Sourcen lassen sich nicht transpilieren. Analog könnte eine Java-Klasse nicht kompilieren werden, wenn die verwendeten Objekte nicht im Classpath liegen. Wie aber kann das funktionieren?

Die Protractor-Sourcen stehen erst im Browser zur Verfügung, wenn wir den Protractor-Test starten. Das Browser-Objekt wird von Protractor als globale Variable zur Verfügung gestellt. Irgendwie müssen wir Typescript mitteilen, dass das Browser-Objekt zur Laufzeit da sein wird und das eine Übersetzung erlaubt ist.

Eine erste einfache Möglichkeit ist, wir deklarieren die Variable manuell mit declare var und geben Typescript einen Hinweis, dass browser zur Laufzeit vefügbar sein wird.

module protractor.testing {

    declare var browser:any;

    export class Test {
        protractorExample() {
            browser.get("http://juliemr.github.io/protractor-demo/")
        }
    }
}

Versuchen wir den Test zu starten. Das Binary um Protractor zu starten findet sich im entsprechenden node_modules Ordner und wurde beim npm install bereits installiert.

./node_modules/protractor/bin/protractor protractor.conf.js

Protractor beschwert sich natürlich sofort.

[launcher] Error: Error: Could not find chromedriver

Wir haben die Abhängigkeiten noch nicht installiert die wir brauchen, um mit den verschiedenen Browser-Instanzen zu kommunizieren. Protractor bringt auch hierfür alles mit.

 ./node_modules/protractor/bin/webdriver-manager update

Updating selenium standalone
downloading https://selenium-release.storage.googleapis.com/2.45/selenium-server-standalone-2.45.0.jar...
Updating chromedriver
downloading https://chromedriver.storage.googleapis.com/2.14/chromedriver_mac32.zip...
...

Der Webdriver-Manager lädt die nötigen Abhängigkeiten in Form von Jars. Um Protractor zu verwenden ist also ein JDK nötig.

Starten wir den Test erneut.

./node_modules/protractor/bin/protractor protractor.conf.js

Using ChromeDriver directly...
[launcher] Running 1 instances of WebDriver

Wir haben noch keine Test-Cases, aber Protractor startet erfolgreich. Definieren wir einen ersten einfachen Smoke-Test.

module protractor.testing {

    declare var browser:any;
    declare var describe:any;
    declare var it:any;

    export class Test {

        public Test() {}

        protractorExample() {
            browser.get("http://juliemr.github.io/protractor-demo/")
        }
    }

    /**
     * simple jasmine spec
     */
    describe("protractor test", () => {
        it("should open angular js page", () => {
            new Test().protractorExample();
        });
    })
}

Die Seite öffnet sich und der Test ist grün.

Interessanterweise erwartet Protractor ein AngularJS auf der Seite. Biegen wir den Link kurzzeitig einfach auf Google um, öffnet Protractor die Google-Seite und schlägt anschließend fehl.

<span style="color: #ff0000;"> Angular could not be found on the page http://www.google.de/</span> 

Auf der zu testenden Seite ist ein Taschenrechner. Perfekt für unsere Zwecke.

Protractor Calculator Testing

Wir möchten das erste Feld ausfüllen. Zunächst brauchen wir etwas, das dieses Element auf der Seite eindeutig identifiziert. Untersuchen wir den DOM.

&lt;input type="text" class="input-small ng-pristine ng-untouched ng-valid" ng-model="first"&gt;

Ein identifizierendes Element könnte ng-model=”first” sein. Protractor unterstützt das selektieren von Elementen anhand ihres Model-Bindings.

 

 protractorExample() {
      browser.get("http://juliemr.github.io/protractor-demo/")
      var firstInput = element(by.model("first")).sendKeys("1");
 }

Diese ganzen Elemente (element, by, sendKeys) sind Typescript bisher nicht bekannt. Der Code kompiliert nicht. Wir können natürlich jedes Element zuvor deklarieren. Das macht die Arbeit aber unnötig kompliziert, alleine schon deswegen weil uns jemand diese Arbeit bereits abgenommen hat.

Definitely Typed

Für alle gägngigen Frameworks bietet das Definitely-Typed Projekt aktuelle Typdeklarationen, die uns erstens die Arbeit mit den Frameworks erlauben ohne manuell alle Elemente deklarieren zu müssen. Viel wichtiger und spannender ist aber die Möglichkeit, die API eines Frameworks mit der IDE zu erkunden.

Definitely Typed bietet ein Tool, mit dem Typdeklarationen ganz einfach geladen und verwendet werden können – das tsd. Zunächst laden wir das Tool über NPM.

npm install --save-dev tsd

Zunächst initialisieren wir das Projekt für TSD.

tsd init

-&gt; written tsd.json
-&gt; written typings/tsd.d.ts

TSD legt eine tsd.json an, in der geladene Typdefinitionen gespeichert werden.

cat tsd.json 
{
  "version": "v4",
  "repo": "borisyankov/DefinitelyTyped",
  "ref": "master",
  "path": "typings",
  "bundle": "typings/tsd.d.ts",
  "installed": {}
}

Mit Hilfe von tsd query lassen sich Typdefinitionen finden.

tsd query "*protractor*"
 - angular-protractor / angular-protractor  

Installieren wir die Typdefinition.

#--save stores the file to the tsd.json
#--resolve loads all transitive definitions
tsd install angular-protractor --save --resolve

#updated tsd.json
cat tsd.json 
{
  "version": "v4",
  "repo": "borisyankov/DefinitelyTyped",
  "ref": "master",
  "path": "typings",
  "bundle": "typings/tsd.d.ts",
  "installed": {
    "angular-protractor/angular-protractor.d.ts": {
      "commit": "2520bce9a8a71b66e67487cbd5b33fec880b0c55"
    }
  }
}

#downloaded definitions
ls typings/angular-protractor 
angular-protractor.d.ts

Alleine durch das Laden der Typdefinition weiß der Typescript-Kompiler noch nichts von deren Existenz und schon gar nicht, wie er die Dateien laden kann und ob er das soll. Wir können Typescript aber sehr einfach anweisen, das zu tun.

Zunächst lassen wir uns eine reference.ts Datei generieren. Diese Datei beinhaltet Referenzen auf alle Sourcen, die der Typescript-Kompiler laden soll, bevor eine Source-Datei kompiliert wird. Diese Option wird über das Attribute reference konfiguriert, wir hinterlegen diese im Gruntfile für das grunt-ts Plugin.

grunt.initConfig({
        ts: {
            default: {
                src: ["src/script/**/*.ts"],
                reference : "src/reference.ts"
            }
        }
    });

Grunt-TS erzeugt im konfigurierten Ordner eine neue Datei “reference.ts” mit folgendem Inhalt. In der reference.ts Datei landen alle Dateien, die dem grunt-ts Plugin bekannt sind. Aktuell ist das lediglich unsere Source-Datei.

//grunt-start
/// 
//grunt-end

Wir erweitern das Glob-Pattern jetzt um die Typdefinitionen von Definitely-Typed.

grunt.initConfig({
        ts: {
            default: {
                src: ["src/script/**/*.ts","typings/**/*.ts"],
                reference : "src/reference.ts"
            }
        }
    });

Lassen wir anschließend die Sourcen nochmals kompilieren sehen wir die neue Referenz, die automatisch eingefügt wurde.

//grunt-start
/// &lt;reference path="script/sample.spec.ts" /&gt;
/// &lt;reference path="../typings/angular-protractor/angular-protractor.d.ts" /&gt;
/// &lt;reference path="../typings/tsd.d.ts" /&gt;
//grunt-end

Die Referenzen inkludieren wir jetzt in unserem Source-File über eine Typescript-Referenz.

/// &lt;reference path="../reference.ts"/&gt;
module protractor.testing {

    export class Test {

        public Test() {}

        protractorExample() {
            browser.get("http://juliemr.github.io/protractor-demo/")
            //var firstInput = element(by.model("first")).sendKeys("1");
        }
    }

    /**
     * simple jasmine spec
     */
    describe("protractor test", () =&gt; {
        it("should open angular js page", () =&gt; {
            new Test().protractorExample();
        });
    })
}

Da wir jetzt die Typdefinitionen haben können wir die Deklaration der browser-variable getrost entfernen. Der Typescript-Kompiler ist beruhigt. Auch die Elemente element und by kennt der Typescript-Kompiler jetzt.

Zusätzlich laden wir die Jasmine-Typdefinition, damit auch describe() und it() funktionieren.

tsd install jasmine --save --resolve

Nach einem erneuten Kompilieren der Sourcen ist die reference.ts automatisch aktualisiert worden und die Datei kompiliert vollständig.

Ein klein wenig stört der relative Pfad auf die reference.ts-Datei. Sollten wir die Source-Datei aus irgendeinem Grund verschieben würde das Kompilieren nicht mehr funktionieren. Für eine Datei ist das problemlos, für 100 Dateien schon nervig. Aus diesem Grund lassen wir uns die relativen Pfade automatisch vom grunt-ts-Plugin generieren.

Statt

/// &lt;reference path="../reference.ts"/&gt;
module protractor.testing {}

schreiben wir einfach

///ts:ref=reference.ts
module protractor.testing {}

und lassen die Sourcen nochmals kompilieren.
Der Kompiler erzeugt uns eine weitere Zeile im Kopf der Datei.

///ts:ref=reference.ts
/// No file or directory matched name "reference.ts" ///ts:ref:generated

Die Datei ist doch da! Was ist das Problem?
Das Grunt-TS Plugin kann nur Dateien referenzieren, die es kennt. Die Datei reference.ts liegt direkt im src-Verzeichnis. Dieses Verzeichnis wird
nicht von den glob-Patterns im Gruntfile erfasst. Erweitern wir die Liste also um die reference.ts und lassen nochmals kompilieren.

 ts: {
            default: {
                src: ["src/reference.ts","src/script/**/*.ts","typings/**/*.ts"],
                reference : "src/reference.ts"
            }
        }

Jetzt sieht die Datei besser aus.

///ts:ref=reference.ts
/// &lt;reference path="../reference.ts"/&gt; ///ts:ref:generated
module protractor.testing {

Verschieben wir die Datei testweise von “src/script” nach “src/script/pagetest” und kompilieren erneut, ohne dass wir an den Sourcen etwas anpassen.
Als Ergebnis steht in der Datei anschließend folgendes:

///ts:ref=reference.ts
/// &lt;reference path="../../reference.ts"/&gt; ///ts:ref:generated
module protractor.testing {

Der Pfad wurde automatisch angepasst. Perfekt!

Zuletzt möchten wir noch sicherstellen, dass bei jedem Build die Typdeklarationen geladen werden – Typdeklarationen checken wir nicht ein – wir halten das Repository so schmal wie nur irgend möglich. Hierfür konfigurieren wir das grunt-tsd Plugin.

npm install --save-dev grunt-tsd

Zusätzlich konfigurieren wir tsd im Gruntfile.

grunt.initConfig({
        ts: {
            default: {
                src: ["src/reference.ts", "src/script/**/*.ts", "typings/**/*.ts"],
                reference: "src/reference.ts"
            }
        },
        tsd: {
            default: {
                options: {
                    // execute a command
                    command: 'reinstall',

                    //optional: always get from HEAD
                    latest: true,

                    // specify config file
                    config: 'tsd.json',

                    // experimental: options to pass to tsd.API
                    opts: {
                        // props from tsd.Options
                    }
                }
            }
        }
    });

Endlich testen

Viel Vorarbeit für den noch zu schreibenden Test. Wir haben Grunt für einen automatischen Build konfiguriert. Haben alles für Protractor vorbereitet. Haben zusätzlich Definitely-Typed integriert. Alles bereit für den ersten Test.

Unser aktueller Zustand ist etwas dürftig.

protractorExample() {
    browser.get("http://juliemr.github.io/protractor-demo/")
    var firstInput = element(by.model("first")).sendKeys("1");
    browser.pause();
}

Lassen wir den Test laufen und prüfen, was passiert. Durch den Aufruf von browser.pause() lassen wir den Browser pausieren und können uns das Ergebnis betrachten.

protractor_sending_inputs

Erweitern wir den Test und machen einen ersten Funktionscheck.

//open page
browser.get("http://juliemr.github.io/protractor-demo/");
//fill form
element(by.model("first")).sendKeys("1");
element(by.model("second")).sendKeys("2");
element(by.id("gobutton")).click();

var resultList = element.all(by.repeater("result in memory"));
//expect 1 result
expect(resultList.count()).toBe(1);
//expect result to be correct
expect(resultList.first().element(by.binding("result.value")).getText()).toBe('3');

Welche Aufrufe möglich sind sehen wir dank Definitely Typed direkt in der IDE.

ide

 Refactor for Simplicity

Der Test funktioniert. Ganz ehrlich – er ist nichtgut. So kann das nicht bleiben. Der Test hat wenig Struktur. Ein bisschen Spaghetti-Code-Style. Der Code ist rein technisch motiviert und lässt die Fachlichkeit nicht erkennen. Was wollen wir mit dem Test ausdrücken? Was ist kaputt, wenn dieser Test rot wird?

Erstes und wichtigstes Pattern, das wir einführen ist das PageObject.

Beim Lesen des Tests sollte die zugrundeliegende Technik abstrahiert sein. Idealer Test ob ein Testcase gut gemach ist –

Kann ich mich mit einem von der Fachabteilung vor dem Code sitzen und die Fachlichkeit diskutieren. Versteht der, was der Test testet?

Nach dem Umbau sieht der Test so aus:

it("it should add 2 operands", () =&gt; {
     calculatorPage.fill(1, 2);
     calculatorPage.submit();
     calculatorPage.verifyResult(3);
     calculatorPage.verifyNumberOfResults(1);
});

Besser? Besser!

Das Page-Objekt implementieren wir so.

class CalculatorPage {

    open() {
        browser.get("http://juliemr.github.io/protractor-demo/");
    }

    fill(first:number, second:number) {
        element(by.model("first")).sendKeys(first.toString());
        element(by.model("second")).sendKeys(second.toString());
    }

    submit() {
        element(by.id("gobutton")).click();
    }

    verifyResult(expected:number) {
        var resultList = this.findResultList();
        //expect result to be correct
        expect(resultList.first().element(by.binding("result.value")).getText()).toEqual(expected.toString());
    }

    verifyNumberOfResults(numberOfResults:number) {
        var resultList = element.all(by.repeater("result in memory"));
        //expect 1 result
        expect(resultList.count()).toBe(numberOfResults);
    }

    private findResultList() {
        return element.all(by.repeater("result in memory"));
    }

}

Testen wir der Vollständigkeit halber noch die weiteren Operationen. Dank des Page-Objektes ist das jetzt ganz einfach.

enum OperationType {
    ADDITION, DIVISION, MODULO, MULTIPLICATION, SUBTRACTION
}

class CalculatorPage {

    open() {
        browser.get("http://juliemr.github.io/protractor-demo/");
    }

    fill(operation:OperationType, first:number, second:number) {
        element(by.model("operator")).$('[value="' + OperationType[operation] + '"]').click();
        element(by.model("first")).sendKeys(first.toString());
        element(by.model("second")).sendKeys(second.toString());
    }

    submit() {
        element(by.id("gobutton")).click();
    }

    verifyResult(expected:number) {
        var resultList = this.findResultList();
        //expect result to be correct
        expect(resultList.first().element(by.binding("result.value")).getText()).toEqual(expected.toString());
    }

    verifyNumberOfResults(numberOfResults:number) {
        var resultList = element.all(by.repeater("result in memory"));
        //expect 1 result
        expect(resultList.count()).toBe(numberOfResults);
    }

    private findResultList() {
        return element.all(by.repeater("result in memory"));
    }

}

/**
 * simple jasmine spec
 */
describe("Calculator", () => {

    var calculatorPage = new CalculatorPage();

    beforeEach(calculatorPage.open);

    it("it should add 2 operands", () => {
        calculatorPage.fill(OperationType.ADDITION, 1, 2);
        calculatorPage.submit();
        calculatorPage.verifyResult(3);
        calculatorPage.verifyNumberOfResults(1);
    });

    it("it should subtract 2 operands", () => {
        calculatorPage.fill(OperationType.SUBTRACTION, 2, 2);
        calculatorPage.submit();
        calculatorPage.verifyResult(0);
        calculatorPage.verifyNumberOfResults(1);
    });

    it("it should multiply 2 operands", () => {
        calculatorPage.fill(OperationType.MULTIPLICATION, 2, 2);
        calculatorPage.submit();
        calculatorPage.verifyResult(4);
        calculatorPage.verifyNumberOfResults(1);
    });

    it("it should divide 2 operands", () => {
        calculatorPage.fill(OperationType.DIVISION, 20, 10);
        calculatorPage.submit();
        calculatorPage.verifyResult(2);
        calculatorPage.verifyNumberOfResults(1);
    });

    it("it should modulo 2 operands", () => {
        calculatorPage.fill(OperationType.MODULO, 5, 4);
        calculatorPage.submit();
        calculatorPage.verifyResult(1);
        calculatorPage.verifyNumberOfResults(1);
    });
});

Das Schöne ist, der Test ist sehr robust. Egal wie sich die Seite vom Markup verändert. Alle Anpassungen beschränken sich definitiv auf das PageObjekt. Die Tests müssen nicht angepasst werden – ausser die Fachlichkeit ändert sich, was bei einem Taschenrechner eher unwahrscheinlich ist.

Fazit

Protractor ist ein extrem wichtiges Tool für die Entwicklung von Angular-Apps. Trotzdem sind Protractor-Tests immer noch Integration-Tests, die keinesfalls Unit-Tests ersetzten können und dürfen.

Genau wir Selenium-Tests ist es extrem wichtig, die Integration-Tests sauber zu strukturieren um sie wartbar zu halten. Page-Objekte sind hier mit Sicherheit das wichtigste, aber bei weitem nicht das einzige Pattern.

Typescript macht definitiv Sinn für Protractor, denn es ist sehr einfach die API zu durchforsten mit Hilfe der IDE.