Nieder mit den Webtests!

Wie man Browser JavaScript richtig testet

Leonard Ehrenfried - http://leonard.io

Freelance Software Consultant

Ich gebe es zu

Der Titel ist ein bisschen reißerisch.

tl;dr: Webtests sind nicht an und für sich schlecht, aber oft das falsche Tool.

Agenda

  1. Disclaimer, Begriffsklärung, Einleitung
  2. Wofür sind Webtests gut?
  3. Was sind die Probleme von Webtests?
  4. Unit Tests für JavaScript
  5. Umsetztung in Backbone und Angular
  6. Schritt für Schritt zu getestetem JavaScript
  7. Q&A

Disclaimer

Vieles, wovon ich spreche, bezieht sich auf Web-Applikationen und nicht auf Webseiten.

Was ist der Untschied?
Wenn der Zweck der Seite ist, Inhalt zu präsentieren, dann ist es eine Webseite.
Wenn es darum geht, mit der Seite zu interagieren, dann ist es eine Web-Applikation.

Testing

Ist kein neuer Trend - Kent Becks SUnit wurde 1998 released

Ziemlich weitläufig als gute Praxis anerkannt

TDD hat sich auf Serverseite durchgesetzt

Browser JS Testing

Sehr viel neuer

Für lange Zeit gab es nicht viel JS, das zu testen war

JS wurde für lange Zeit nicht ernst genommen, aber das hat sich sehr geändert

JS ist schwach typisiert; Tests sind viel wichtiger

Kleiner Exkurs: Testpyramide

test pyramid http://martinfowler.com/bliki/TestPyramid.html

Was ist ein Webtest?

Webtests sind die Test, die den gesamten Userablauf inklusive der Interaktion mit dem Browser testen.

Ziel: Fährt das Auto?

Das bei weitestem beliebteste Tool ist Selenium/Webdriver.

Nachteile von Webtests

  • Langsam: Tests werden nicht vor dem Commit ausgeführt
  • Wartungsaufwändig: Asynchronität führt zu vielen Problemen
  • Sehr grobe Granularität: Unklar, was den Test fehlschlagen lassen hat
  • Nonlokalität: schwierig ein Szenario unter Test aufzubauen
  • Nondeterministisch und flaky: Viele false negatives, Entwickler fangen an fehlgeschlagene Tests zu ignoreren
  • Schlechte Debuggability: Man kann nicht durch die Tests steppen, da der Browser unabhängig vom Testrunner ist
  • Keine Unit Tests, sondern Scenariotests

Wofür sind Webtests gut?

Einmal alle Schichten durchzutesten

Happy path

Testbarkeit

Server-seitiger Code von 1998: wahrscheinlich nicht besonders testbar geschrieben

  • Globale Variablen
  • Statische Methoden
  • Feste Abhängigkeit

Verbesserungen

  • DI
  • Lokale Variablen
  • Polymorphism, "ask, don't tell"

Problemfälle der JS-Testbarkeit

  • DOM-Manipulation
  • AJAX
  • Animationen, setTimeout()

Probleme: DOM

  • Direkter Zugriff aufs DOM ist Gift für Testbarkeit!
  • 
    $(".datepicker").datePicker();
                  

Lösungen: DOM

  • Komponenten produzieren nur HTML oder DOM-Knoten
  • 
    var datePicker = new DatePicker();
    var rendered = datePicker.render();
    $("#date-picker").append(rendered);
                  
  • Kompenenten dürfen nur auf ihrem "eigenen" HTML operieren
  • Bedeutet natürlich, dass serverseitige Templatingsprachen die Testbarkeit radikal verschlechtern

HTML und JS

  • HTML enthält oft Logik
  • Wird selten mitgetestet
  • HTML-Schnipsel nicht global aus dem DOM holen!
  • Als explizite Abhängigkeiten definieren, z.B. mit requireJS

Beispiel RequireJS Text Plugin


define(["text!templates/view.html"], function(template){
  return function(){
    var div = $("<div>").html(template);
    div.delegate("a", "click", function(){
      div.find("h1").css("color", "red");
    });
    return div;
  };
});
          

Probleme: AJAX

  • Benötigt laufenden Server, der Antworten zurückschickt
  • Macht Testsetup komplizierter und -ausführung langsamer
  • Ziel sollte sein, Tests ohne Server laufen zu lassen

Lösungen: AJAX

  • In server-seitigem Code mit DAO-Schicht gemockt
  • Globales XMLHttpRequest monkey-patchen
  • Oder Mock HTTP-Service injecten

Exkurs: sinon.js

Sehr beliebte Library, um HTTP zu mocken


var fakeAjax = function(func) {
  var xhr = sinon.useFakeXMLHttpRequest();
  var requests = [];
  xhr.onCreate = function(request){
    requests.push(request);
  }
  func(requests);
  xhr.restore();
});
            

Anwendung


fakeAjax(function(requests){
  var customer = new Customer({ id:1234 });
  customer.fetch();
  var req = requests[0];
  req.respond(200, {id : 1234, name: "Horst Kasuppke"});

  expect(customer.get("name")).toBe("Horst Kasuppke");
});
            

Führt zu einem deterministischen Test ohne implizites Warten

Probleme: Animationen, setTimeout()

  • Animationen arbeiten oft mit setTimeout()

var callbackCalled = false;
jasmine.Clock.useMock();
setTimeout(function() {
  callbackCalled = true;
}, 100);
jasmine.Clock.tick(101);
expect(callbackCalled).toBe(true);
            
  • Tests könnnen ohne implizite Waits ausgeführt werden und bleiben blitzschnell

Fazit bis hierhin

JS ist oft höchst asynchron.

Mit einem zweiten Programm/VM sinnvolle Asserts zu schreiben ist extrem wartungsaufwändig.

Um schnelle Unit Tests für Browser JS zu schreiben, müssen wir viel näher an den Code, als es mit Selenium möglich ist.

Um Frontendcode testbar zu machen, müssen wir ihn aus den Klauen des Servers befreien.

MVC Frameworks

Backbone

  • Sehr frühes, minimalistisches Frontend-MVC Framework
  • Gute Wahl für Apps mit niedriger Komplexität und recht wenig Features
  • Sehr testbar, da sehr simpel strukturiert
  • Muss sich allerdings Disziplin selbst auferlegen

Wie testet man eine Backbone View?


fakeAjax(function(requests){
  var view = new FooView();
  view.render();
  var req = requests[0];
  req.respond(200, [
    { name: "Guido van Rossum" },
    { name: "Brendan Eich" }]);
  view.$el.find("button").click();
  expect(view.$el.find("ul li").size()).toBe(2);
});
          

Angular

  • Im Moment der Darling der JavaScript-Welt
  • Generell sehr testbar aufgebaut: MVC, DI, Separation of Concerns, Controllers, Mocks
  • Autor (Miško Hevery) ist ein bekannter Testing-Coach

Angulars Stärken

  • Sehr mächtige Templating-Sprache
  • Testen wird sehr groß geschrieben
  • Alle Abhängigkeiten können injected werden
  • ajax, window, document, setTimeout können einfach gemockt werden

Schwächen (nicht viele)

  • Steile Lernkurve
  • Controllers werden ohne HTML-Templates getestet
  • Verlässt sich auf sogenannte "end-to-end"-Tests
  • Fühlt sich wie ein Webtest an

Engines: wie führe ich meine Tests aus?

  • Während der Entwicklung: Browser (Demo)
  • PhantomJS: headless Webkit
  • Experimentell: node.js mit jsdom, contextify
  • Gründlich, aber aufwendig: In allen Zielbrowsern

Schritt für Schritt zu einem testbaren Frontend

Konzeptionelle Verbesserungen

  • In vielen Software-Shops wird zuletzt über das Frontend gedacht
  • Am wichtigsten: Bewusstsein für Frontend-Probleme schaffen
    • Frontend-Entwicklung zu einer Priorität machen
    • Gleiche Rigorosität anwenden wie für die Backend-Entwicklung
    • Frontend-Spezialisten einstellen
  • Toxischen IE-Support beenden

Sorgen der Frontend-Entwickler ernst nehmen.

Technische Verbesserungen

Wie legt man einen Sumpf trocken?
  • Ersten Unit Test schreiben
  • CI für JS aufsetzen
  • Limitationen der Testbarkeit der Applikation kennenlernen
  • Neue Features test-getrieben entwickeln
  • Langer steiniger Weg

Meine Empfehlungen für Libraries

  • Jasmine als Testframework
  • Sinon für HTTP Mocking
  • Handlebars für client-side Templating
  • Backbone für kleinere Apps
  • Angular für komplexere Apps

Zusammenfassung

Um einen Scheibenwischer zu testen, muss man keine Probefahrt im Regen machen.

THE END - FRAGEN?

Leonard Ehrenfried

Slides: http://leonard.io/talks/

Hire me: http://leonard.io/hire-me/