Über unsMediaKontaktImpressum
Heiko Spindler 12. Januar 2015

Code-Qualität trotz JavaScript

JavaScript hat sich in den letzten Jahren von einem Hilfsmittel für die Dynamisierung von Webseiten mit einfacher Logik hin zu einer Plattform für ernsthafte Businesslogik entwickelt. Die Sprache hat die reine Browser-Entwicklung mit Frameworks wie jQuery hinter sich gelassen und ist auf dem Server über die Datenbank bis hin zu Microcontrollern [1] verfügbar. Einige Statistiken, wie die Anzahl der Repositories bei Github oder die Anzahl der Fragen bei Stackoverflow, zeigen das große Interesse an der Sprache und seine aktive Fangemeinde.

Großen Anteil an dieser Entwicklung hat die Entscheidung von Google, den Browser als die grundlegende Applikationsbasis zu etablieren. So bietet Google nicht nur sein komplettes Office-Paket als Web-Applikation, sondern mit Chrome OS ein komplettes Betriebssystem mit dem Browser als zentralem Steuerungselement.

Der serverseitige Durchbruch ist auf Nodejs [2] und sein interessantes Programmiermodell (event-driven, non-blocking) und die sehr agile und produktive Community zurückzuführen. So gibt es aktuell über 95.000 registrierte Nodejs-Module [3].

Die Qualität und Nutzbarkeit der Frameworks für JavaScript hat sich in den letzten Jahren deutlich verbessert. Bibliotheken wie zum Beispiel AngularJS [4], Backbone und Express bieten die Möglichkeit, umfangreiche Applikationen gut zu strukturieren. Dependency Injection, Modularisierung, Datenbindung und eine gute Testbarkeit haben mit diesen Frameworks in die JavaScript-Welt Einzug gehalten.

Gerade die Implementierung unternehmenskritischer Applikationen in die Entwicklung mit JavaScript führt zu neuen Anforderungen für den Einsatz der Sprache. Die Applikationen werden sehr viel größer, komplexer und sind langfristiger im Einsatz. Damit geht oft einher, dass größere Teams und mehr Personen langfristig mit ihnen arbeiten. Das funktioniert in den meisten Fällen nur, wenn man den Themen Qualität, Nachvollziehbarkeit, Nachhaltigkeit und Dokumentation eine größere Bedeutung beimisst. Das wirft die Frage auf, ob es in der JavaScript-Welt alle notwendigen Voraussetzungen gibt, um dieses Ziel zu erreichen.

Konventionen

Damit sich größere und verteilte Teams besser verstehen, sollten Konventionen für die Formulierung von Source Code und Dokumentationen eine Selbstverständlichkeit sein. Bei vielen anderen Sprachen ist dieses Mittel anerkannt und weitverbreitet. In Web-Projekten gilt es meist als sehr viel Freiheit und Individualität. Grundsätzlich ist diese Freiheit gut, führt aber in extremen Fällen ebenfalls leicht zu Wildwuchs.

Gute Startpunkte sind die Source Code-Konventionen von Google [5]. Diese bieten neben den eigentlichen Regeln mit Beispielen ebenfalls die Begründung, warum diese Regel sinnvoll ist und angewendet werden sollte.

Im Projektalltag wir es immer wieder zu Situationen kommen, in denen Anpassungen sinnvoll sind. Somit ist der Guide von Google eine Vorlage für projekt- oder unternehmensspezifische Vorgaben.

Hier eine kleine Liste mit weiteren Vorgaben zusätzlich zu reinen Source Code-Konventionen:

  • Verzeichnisstruktur,
  • Vorgaben für Kommentare,
  • Sprache im Source Code,
  • Namenskonventionen für Variablen, Funktionen, Dateien,
  • Eingesetzte Frameworks mit Versionen,
  • Regeln für manuelle Code-Reviews

Dokumentation mit JSDOC

Dokumentieren ist sicherlich keine besonders beliebte Tätigkeit in der Entwicklergemeinde. Guter Programmtext sollte möglichst selbsterklärend sein. Trotzdem gehören bei der professionellen Entwicklung von wartbarer und nachhaltig nutzbarer Software zusätzliche Kommentare zum guten Ton. Spätestens wenn man Programmteile als Bibliothek anderen Projekten oder gar Kunden zur Verfügung stellt, ist die Beschreibung der Schnittstelle oder API eine Pflicht.

Ein gutes Hilfsmittel ist JSDOC [6]. Es bietet eine Syntax und Konventionen für das Erstellen von Kommentaren und die Beschreibung von Dateien und Funktionen. Dabei können die Signaturen der Funktionen zusätzlich mit Typinformationen für Parameter und Rückgaben angereichert werden. Ähnlich zu JavaDoc erzeugt JSDOC aus den Kommentaren eine leicht navigierbare HTML-Seite mit allen Informationen.

JSDOC basiert auf Nodejs und kann leicht mit NPM für ein Projekt oder global (mit dem Schalter -g) installiert werden. Der Aufruf bei der globalen Installation erfolgt mit dem Kommando jsdoc und der Angabe der Datei oder des Verzeichnisses, für das die Dokumentation generiert werden soll:

jsdoc yourJavaScriptFile.js

Die folgende Tabelle listet eine kleine Auswahl wichtiger JSDOC-Angaben auf. Diese beginnen mit einem @-Zeichen. Geschweifte Klammern zeigen Typinformationen. In eckigen Klammern stehen optionalen Angaben:

JSDOC-Angabe Beschreibung
@param {Typ} Beschreibung des Parameters Beschreibung eines Parameters mit Datentyp und Name.
@return {Typ} Beschreibung der Rückgabe Beschreibt die Semantik der Rückgabe und den Type einer Funktion.
@fires #[event:] Dokumentiert die Events, die eine Funktion emittieren kann.
@throws {Typ} Beschreibung Beschreibt eventuell auftretende Ausnahmen (Exceptions) bei der Ausführung der Funktion.
@author [] Hinterlegt Angaben zum Autor.
@deprecated [] Markiert die Funktion als veraltet mit einer optionalen Beschreibung.

Neben dem an JavaDoc angelehnten Style gibt es andere Vorlagen.

Das folgende Fragment zeigt JavaScript-Code mit typischen Metainformationen in der JSDOC-Syntax:

javadoc_report (Syntaxhighlighter)


/**
 * Erzeugt eine Instanz der Klasse Kreis.
 *
 * @constructor
 * @this {Kreis}
 * @param {number} r Der Radius des Kreises.
 */
function Kreis (r) {
    /** @private */ this.radius = r;
    /** @private */ this.umfang = 2 * Math.PI * r;
}
 
/**
 * Erzeugt eine Instanz der Klasse Kreis mit dem Durchmesser.
 *
 * @param {number} d Der gewünschte Durchmesser des Kreises.
 * @return {Kreis} Liefert die Instanz.
 */
Kreis.vomDurchmesser = function (d) {
    return new Kreis (d / 2);
};
 
/**
 * Berechnet den Umfang.
 *
 * @deprecated
 * @this { Kreis }
 * @return {number} Liefert den berechneten Umfang.
 */
Kreis.prototype.berechneUmfang = function () {
    return 2 * Math.PI * this.radius;
};

In der folgenden Abbildung ist eine Beispiel Hilfeseite zu sehen, die mit JSDOC generiert wurde:

Statische Code-Analyse und Metriken

Statische Code-Analyse ist ein einfaches und günstiges Mittel, um frühzeitig und automatisch Schwachstellen zu identifizieren. In manchen Fällen ist es sogar möglich, echte Programmierfehler auszumerzen. Sicherlich kann der Einsatz bei statisch typisierten Sprachen (wie zum Beispiel Java) weitergehen, da die frühzeitige Festlegung von Typen genauere Auswertungen auf Codeebene möglich macht.

Metriken bewerten Teile des Codes und liefern quantitative Aussagen. Eine sehr primitive Metrik ist die Anzahl der Codezeilen in einer Datei oder Funktion. So sollte man eine JavaScript-Funktion kritisch betrachten, die länger als ein gesundes Maß ist: Mehr als 70 bis 100 Zeilen machen oft wenig Sinn und deuten darauf hin, dass eine Funktion evtl. unterschiedliche oder zu viele Aufgaben in sich vereint. Eine Aufteilung in kleinere Einheiten mit treffenden Namen hilft, den Programmtext besser zu verstehen.

Für die statische Analyse von JavaScript gibt es mehrere Werkzeuge. JSLint [7] existiert schon seit einigen Jahren und bietet eine Online-Oberfläche. Zur Analyse kopiert man den Programmtext einfach in die Textbox und schaltet die gewünschte Analyse ein. Das etwas neuere Projekt ESLint [8] stellt eine offene und erweiterbare Basis für die statische Codeanalyse für JavaScript dar. Das Projekt steht unter einer Open-Source-Lizenz. Die Regeln können leicht konfiguriert und erweitert werden. Es bietet mehr Regeln als JSLint an und kann leicht automatisiert werden. ESLint steht als Nodejs-Modul bereit und lässt sich leicht über die Kommandozeile als NPM installieren:

npm install -g eslint

Danach startet man mit dem Befehl "eslint" die Analyse für einzelne Dateien oder ganze Verzeichnisse:

eslint [options] [file|dir]*
eslint test.js test2.js

Die Ausgabe erfolgt direkt auf der Konsole. Im folgenden Beispiel sind jeweils die Zeilennummer, Kritikalität (Fehler oder Warnung), Beschreibung und die entsprechende Regel (als Kurzname) zu sehen, gegen die verstoßen wurde:

 38:13 error 'require' is not defined                           no-undef 
 43:11 error 'require' is not defined                           no-undef 
515:69 error 'serviceContainer' is not defined                  no-undef 
 710:0 error 'exports' is not defined                           no-undef 
  36:0 error Use the function form of "use strict"              no-global-strict 
  36:0 warning Strings must use doublequote                     quotes 
152:36 error It's not necessary to initialize 'cb' to undefined no-undef-init 
173:63 warning Strings must use doublequote                     quotes 
193:36 error It's not necessary to initialize 'cb' to undefined no-undef-init 
389:43 warning Strings must use doublequote                     quotes 
491:15 warning Expected '!==' and instead saw '!='              eqeqeq 
568:13 warning Unnecessary semicolon                            no-extra-semi 
203:93 error args is defined but never used                     no-unused-vars 

Die Konfiguration von ESlint erfolgt in einer JSON-Datei. Regeln können einzeln aktiviert oder deaktiviert werden und für jede Regel lässt sich die Kritikalität steuern, entweder als Fehler oder als Warnung. Somit kann man für eigene Projekte genau steuern, was man prüfen möchte und eventuell  Regeln zeitlich versetzt einführen.

Der folgende Auszug zeigt die Konfiguration einiger Regeln, die von der Voreinstellung abweichen sollen. Im Abschnitt „rules“ sind die Regeln mit ihren Namen aufgeführt. Die zugewiesenen Werte legen fest, ob die Regel abgeschaltet ist (0), als Warnung (1) oder als Fehler (2) angewendet wird:

{
    "env": {
        "browser": true
    },
    "rules": {
        "eqeqeq": 1,
        "strict": 1,
        "quotes": 0,
        "no-extra-semi": 1
    }
}

Den Namen einer solchen Konfigurationsdatei erwartet ESLint bei Angabe des Parameters –c:

eslint -c myconfig.json model.js

Die Regeln für ESLint selbst sind ebenfalls in JavaScript implementiert und die Schnittstelle ist recht leicht nachvollziehbar. Der Code kann auf der Projektseite eingesehen werden und es gibt eine brauchbare Dokumentation als Einstieg in die Entwicklung eigener Regeln oder Metriken.

Die folgende Tabelle listet einige bekannte Regeln auf. Die vollständige Liste mit zusätzlichen Angaben ist auf der Github-Seite zu finden:

Name der Regel Beschreibung
no-cond-assign Verbietet direkte Zuweisungen in Ausdrücken für IF-Statements oder anderen Prüfungen.
no-dupe-keys Meldet doppelte Attribute in Objekt Literalen.
no-unreachable Verbietet weitere Statements, die nicht erreichbar sind, zum Beispiel Anweisungen nach einem Return, Throw, Continue oder Break.
complexity Definiert die erlaubte maximale zyklomatische Komplexität einer Funktion.
eqeqeq Erzwingt den Einsatz von === und !== für Vergleiche. Da dieser Komparator keine impliziten Typumwandungen durchführt.
no-eval Verbietet den Einsatz der Eval-Funktion, die beliebigen JavaScript ausführt.
no-redeclare Verweist auf mehrfaches Deklarieren einer Variablen.
strict Erzwingt den Einsatz des Strict-Mode. In diesem Modus prüft der ausführende JavaScript-Interpreter den Code strenger und erlaubt bestimmte fehleranfällige Sprachkonstrukte von JavaScript nicht.
no-unused-vars Zeigt Variablen, die deklariert, aber nicht genutzt wurden.
max-depth Meldet Funktionen, die zu tief geschachtelt sind.
max-params Identifiziert Funktionen, die zu viele Parameter deklarieren.
max-statements Spezifiziert die maximale Anzahl von Anweisungen in einer Funktion und meldet Funktionen, die diese Anzahl überschreiten.

Sonar Qube

Der Einsatz von ESLint begrenzt sich natürlich nur auf die Sprache JavaScript. In der Realität scheinen immer mehr Projekte unterschiedliche Sprachen im selben Projekt zu nutzen. So könnte das Backend mit Java EE-Mitteln realisiert sein und die Benutzer greifen über HTML-Client auf diese Funktionalität zu. Damit entsteht der Wunsch nach einem einheitlichen Ansatz für die Code-Analyse über alle eingesetzten Sprachen hinweg.

Sonar Qube [9] löst genau dieses Problem und vereint viele isolierte Analyse-Werkzeuge in einer Datenbank und unter einer Web-Oberfläche. Es steht unter einer Open-Source-Lizenz, das Team bietet aber zusätzlichen kostenpflichtigen, professionellen Support sowie Erweiterungen an. Die Anzahl der analysierbaren Sprachen beläuft sich auf mehr als 20, wobei nicht alle kostenfrei sind. Unter der Haube bedient sich Sonar Qube bekannter Werkzeuge, wie dem angesprochenen ESLint oder Finbugs aus der Java-Welt.

Unter der Web-Adresse der Demo-Seite NEMO [10] sieht man die Analysen und die Navigation online in Aktionen ohne eine eigene Installation zu erstellen. Das Projektteam analysiert einige bekannte Open-Source-Projekte mit Hilfe von Sonar.

Testen und testgetriebene Entwicklung

Testgetriebene Entwicklung ist ein guter Ansatz, um auch umfangreiche Projekte handhabbar zu halten, bei denen nicht alle Abhängigkeiten immer in allen Details durchschaubar sind. Dabei zeigen sich die wirklichen Vorteile des Ansatzes vor allem, wenn sich Änderungen im Projekt ergeben. Funktionieren die vorhandenen Unit-Testfälle nach der Anpassung weiterhin, bekommt das Entwicklungsteam frühzeitig Feedback, dass die Änderungen keine negativen Auswirkungen in anderen Teilen der Anwendung haben.

In den letzten Jahren sind für JavaScript die notwendigen Frameworks entstanden. So bietet QUnit [11] die Grundlage, um Unit-Tests für jQuery zu erstellen. Jasmine [12] geht noch einen Schritt weiter und erlaubt das Erstellen von Behavior Driven Development (BDD). Dabei startet man beim Erstellen des Tests von natürlich-sprachlichen Aussagen, die direkt den Anforderungen für das System entnommen werden könnten. Daraus leitet man die Beschreibung der Testfälle ab. Der Report der ausgeführten Testfälle mit dem Ergebnis, ob der Test positiv oder negativ war, liest sich wie ein Katalog der Anforderungen.

Installieren kann man Jasmine, indem man die entsprechende Bibliothek im Projekt ablegt und folgende Verzeichnisstruktur aufbaut:

Verzeichnis Inhalt
/lib Die Jasmine Bibliothek mit den Dateien: Jasmine.js, jasmine-html.js und jasmine.css
/spec Beschreibung des Tests in der Datei WarenkorbSpec.js
/scr Skript mit dem zu testenden Code (Warenkorb.js)
/ Einstiegspunkt für den Test ist die HTML-Datei „SpecRunner.html“, die alle anderen Elemente referenziert und den Testlauf startet.

Die Testspezifikation

Als Beispiel erstellen wir eine einfache Klasse mit grundlegenden Warenkorbfunktionen:


/* Beispiel: Produkte */
var produkte = [
   {  
   id: 1,
    name: "HirnSport.de Buch",
    preis: 8.50,
    mwst: 7
  },
  // weitere Beispiele…
];

var Warenkorb = function () {

  // Abbildung der Produktliste
  var produktListe = [];

  // Findet eine Position üner der ProduktId
  var findById = function (id) {
    for (var i = 0; i < produktListe.length; i++) {
      if (produktListe[i].item.id == id) {
        return produktListe[i]
      }
    }
    return null;
  }

  /** Schnittstelle des Warenkorbs */
  return {
    addProdukt: function (einProdukt, bestellMenge) {

      var item = findById(einProdukt.id);
      if (!item) {
        produktListe.push({ item: einProdukt, menge: bestellMenge });
      } else {
        item.menge = item.menge + bestellMenge;
      }
    },

    getProduktListe: function () {
      return produktListe;
    },

    leeren: function () {
      produktListe = [];
    }
  };
}

Der Warenkorb ist nur angedeutet, stellt drei Funktionen bereit, um Positionen einzufügen, die aktuell enthaltenen Positionen zu liefern und den Work komplett zu leeren. Die globale Liste „Produkte“ definiert Beispielprodukte für den Test.

Mit Jasmine soll die korrekte Arbeitsweise des Warenkorbs getestet werden. Dafür steht hierfür eine eigene Sprache mit Befehlen bereit:

Der Funktionsausruf „describe()“ fasst eine Gruppe von einzelnen Tests unter einem Thema („Warenkorb“) zusammen. Die einzelnen Tests leitet die Funktion „it()“ ein und prüft jeweils einen klar umrissenen Aspekt des Verhaltens. Auch die Funktion „it()“ erwartet eine textuelle Beschreibung, zum Beispiel „ist nach seiner Erzeugung leer.". Somit entsteht aus dem Thema und dem konkreten Test ein vollständiger Satz „Warenkorb ist nach seiner Erzeugung leer.“. Diese Sätze ergeben sich im generierten Report als Ergebnis des erfolgreichen Tests.

Mit der Zusicherung expect(… Ausdruck …) stellt man im Testfall eine Erwartung auf, welchen konkreten Wert der Ausdruck entsprechen soll. Mit der Prüfung .toEqual([]) testet Jasmine, ob der Ausdruck einem leeren Array entspricht oder mit .toBe(1), ob der Wert genau 1 ist. Zusicherungen entsprechen Asserts in Java Unit-Tests.

Der konkrete Test prüft die Funktionalität des Warenkorbes in mehreren Schritten ab. Die Funktion beforeEach() initialisiert den Warenkorb vor jeder Prüfung erneut. Die erste it()-Funktion prüft, dass der Warenkorb existiert und korrekt initialisiert wurde. Danach werden Positionen eingefügt und geprüft, ob die neue Position aufgenommen wurde. Enthält der Warenkorb denselben Artikel mehrfach, soll nur eine Position mit angepasster Menge existieren. Die Datei WarenkorbSpec.js formuliert diese Testfälle.


describe("Warenkorb", function() {
  var warenkorb;
   
  // Initialisiert den Warenkorb vor jedem Test neu
  beforeEach(function() {
    warenkorb = new Warenkorb();
  });
   
  it("ist nach seiner Erzeugung leer.", function() {
    expect(warenkorb.getProduktListe()).toEqual([])
  });
   
  it("kann Produkte aufnehmen.", function() {
    expect(warenkorb.getProduktListe()).toEqual([])
    warenkorb.addProdukt( produkte[0], 1 );
    expect(warenkorb.getProduktListe().length).toBe(1);
  });
   
  it("kann Produkte aufnehmen und enthält die aufenommen Produkte.", function() {
    expect(warenkorb.getProduktListe()).toEqual([])
    warenkorb.addProdukt( produkte[0], 1 );
    expect(warenkorb.getProduktListe()[0].item).toEqual(produkte[0])
  });
   
  it("legt gleiche Produkte zusammen und addiert die Menge in der Position.", function() {
   
    // Warenkorb ist leer.
    expect(warenkorb.getProduktListe()).toEqual([])
    expect(warenkorb.getProduktListe().length).toBe(0);
   
    // Erstes Produkt einfügen.
    warenkorb.addProdukt( produkte[0], 1 );
    expect(warenkorb.getProduktListe().length).toBe(1);
   
    // Weitere 3 Exemplare vom selben Produkt einfügen.
    warenkorb.addProdukt( produkte[0], 3 );
   
    // Es gibt weiterhin nur eine Position...
    expect(warenkorb.getProduktListe().length).toBe(1);
   
    // ... vom selben Produkt mit der addierten Menge.
    var position = warenkorb.getProduktListe()[0];
    expect(position.menge).toBe(4);
   
  });
…  
});
Der folgende Screenshot zeigt die Ausgabe eines Testlaufs:
Jasmine hat noch sehr viel mehr zu bieten. Die Liste der Methoden für das Prüfen von bestimmten Zuständen ist sehr umfangreich. Außerdem können Funktionsaufrufe leicht überwacht werden. Im Zusammenspiel mit den Framework Istanbul [13] ist es sehr leicht, die Testüberdeckung (code coverage) zu kontrollieren. Welche Anweisungen und welche Ausführungspfade werden durch vorhandene Testfälle geprüft und welche Teile fehlen eventuell noch. Somit erhält man sehr schnell eine vollständige Abdeckung des Programmcodes.

Entwicklungswerkzeuge und Editoren

Obwohl JavaScript schon einige Jahre eingesetzt wird und sich intensiviert hat, gibt es wenige gute Editoren. Viele Editoren bieten grundlegende Funktionen wie Syntaxhervorhebung an. Sehr viel weiter gehen diese Funktionen oft nicht. Ausnahmen sind JetBrains WebStorm [14] und Eclipse JSDT [15]. Ausgefeilte Refactoring Funktionen, die zum Beispiel alle Referenzen einer Funktion im gesamten Projekt analysieren und ändern können, sucht man vergebens. Sehr dankbar ist man als JavaScript- oder Web-Entwickler für die gute Unterstützung in den diversen Browsern. Entweder sind die Funktionen schon vom Start weg installiert, wie die Chrome Developer Tools [16] oder die Internet Explorer Developer Toolbar oder sie können über Erweiterungen als Plugin eingefügt werden, wie die Firebug-Erweiterung für Firefox [17]. In jedem Fall stehen damit Debugging von JavaScript, DOM-Inspektionen und das Verfolgen der Netzwerk-Aktivitäten und Timing zur Verfügung.

Fazit

Der Artikel konnte nur eine erste Auswahl der Möglichkeiten beleuchten. Für professionelle JavaScript-Projekte gibt es hinsichtlich Maßnahmen zur Verbesserung der Qualität keine ausreden. Sowohl Vorgehen als auch Werkzeuge und Framework sind ausreichend und in guter Qualität vorhanden. Die Projektteams und Entwickler können sich bei dieser Auswahl bedienen und müssen das Rad nicht neu erfinden.
Autor

Heiko Spindler

Heiko Spindler ist als Software-Entwickler und Software-Architekt tätig. Er ist zertifizierter ScrumMaster (CSM) und Dozent an der Fachhochschule Gießen-Friedberg, schreibt für Fachmagazine und präsentiert neue Entwicklungen auf…
>> Weiterlesen
Kommentare (0)

Neuen Kommentar schreiben