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 eines Parameters mit Datentyp und Name. |
@return {Typ} Beschreibung der Rückgabe | Beschreibt die Semantik der Rückgabe und den Type einer Funktion. |
@fires | 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);
});
…
});