Heute Abend hat uns einer meiner Kollegen in einer unserer Wissensaustausch-Sitzungen herausgefordert, eine TagCloud in JavaScript zu schreiben. Er hatte ein schönes Setup vorbereitet, bei dem ein Server Twitter-Hashtags über einen WebSocket an den Browser sendet und Processing.js verwendet, um eine grafische Darstellung der Tags zu erzeugen, die auf Twitter vorbeiziehen. Da er bereits die ganze schwere Arbeit der Integration all dieser schicken neuen Frameworks erledigt hatte, fragten Sie sich vielleicht, was noch zu tun war. Nun, wir mussten noch einen Algorithmus implementieren, der die Anzahl der Tags im kontinuierlichen Stream zählt, diese Liste auf der Grundlage der Zählungen sortiert und sicherstellt, dass dem System nicht der Speicher ausgeht, indem weniger genutzte Tags auf intelligente Weise entfernt werden. Er wollte damit sagen, dass JavaScript zwar in manchen Kreisen als die neue-alte-neue Sprache der Zukunft gepriesen wird, das Schreiben und Testen eines nicht-trivialen Algorithmus in dieser Sprache jedoch eine große Herausforderung darstellt.
In der verbleibenden Stunde versuchten wir in mehreren Paaren, den fehlenden Algorithmus zu implementieren, und man muss sagen, dass wir alle kläglich scheiterten. Es muss gesagt werden, dass keiner von uns JavaScript-Experten war, aber ich hatte (anfangs...) das Gefühl, dass ich genug Erfahrung in der IT im Allgemeinen und ein wenig in JavaScript haben sollte, um dies zu schaffen. Aber leider... Als ich nach Hause fuhr, beschloss ich, nicht so leicht aufzugeben und zu versuchen, einige anständige Frameworks zu verwenden, um diese Herausforderung zu meistern.
JavaScript-Tests: YUI
Ich habe mich zunächst auf die Suche nach einem Test-Framework gemacht. Natürlich konzentrieren sich die meisten JavaScript-Test-Frameworks auf ausgefallene UI-Funktionen wie asynchrone Tests, Integrationstests oder automatisierte Multi-Browser-Tests. Ich suchte nichts davon, sondern ein einfaches, xUnit-ähnliches Framework, das ich in einem Browser ausführen konnte, um die Funktionalität einer Reihe von JavaScript-Klassen zu testen. Nach ein paar Minuten fand ich YUI, das genau das Richtige zu sein schien. Die Einrichtung war einfach genug:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "https://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Testing TagCounter with YUI</title>
</head>
<body class="yui-skin-sam">
<div id="yui-main"><div id="testReport"></div></div>
<script type="text/javascript" src=" https://yui.yahooapis.com/3.0.0/build/yui/yui-min.js "></script>
<script type="text/javascript" src="js_cols/base.js"></script>
<script type="text/javascript" src="TagCounter.js"></script>
<script type="text/javascript" src="TagCounter_Test.js"></script>
</body>
</html>
Diese statische HTML-Datei ist alles, was ich brauche, um meine Tests auszuführen. Die Tests selbst werden in der Datei TagCounter_Test.js geschrieben:
/**
* Unit Tests for TagCounter
*/
YUI({
combine: true,
timeout: 10000
}).use("node", "console", "test", function (Y) {
var assert = Y.Assert;
var TagCounterTestCase = new Y.Test.Case({
// test case name - if not provided, one is generated
name: "TagCounter Tests",
"test should increment count": function () {
var tc = new TagCounter();
tc.add("my-tag");
assert.areEqual(1, tc.map().get("my-tag"));
tc.add("my-tag");
assert.areEqual(2, tc.map().get("my-tag"));
}
});
//create the console
var r = new Y.Console({
newestOnTop : false,
style: 'block'
});
r.render("#testReport");
Y.Test.Runner.add(TagCounterTestCase);
Y.Test.Runner.run();
});
Wenn Sie die HTML-Datei in FireFox öffnen, sehen Sie das folgende Bild.
Es scheint, als wäre ich bereit, den TagCounter TDD-Stil zu implementieren!
Implementierung: js_cols
Damit dieser einfache Test erfolgreich ist, brauche ich nur eine einfache HashMap mit einigen grundlegenden Funktionen wie contains, get und remove, aber... das ist nicht Java! Nach einigem Googeln bin ich auf
js_cols.require('js_cols.LinkedHashMap');
function TagCounter(maxSize) {
this.tagCounts = new js_cols.LinkedHashMap(maxSize, true);
};
TagCounter.prototype.add = function (tag) {
if (this.tagCounts.contains(tag)) {
this.tagCounts.insert(tag, this.tagCounts.get(tag) + 1);
} else {
this.tagCounts.insert(tag,1);
}
};
TagCounter.prototype.map = function () {
return this.tagCounts;
};
Letztendlich möchten wir jedoch, dass die Map-Methode nur die beliebtesten Tags zurückgibt, während unser Zähler eine längere Liste verfolgen soll. Um dies umzusetzen, benötigen wir zwei Schritte:
- Zunächst müssen wir die Tags nach der Anzahl ihres Auftretens in absteigender Reihenfolge sortieren, wobei wir berücksichtigen, dass mehrere Tags die gleiche Nummer haben können.
- Dann müssen wir die Top x der beliebtesten Tags nehmen.
Der Test:
[javascript]
...
"test should return limited sized map": function () {
var tc = new TagCounter(3);
tc.add("my-old-tag");
tc.add("my-old-tag");
tc.add("my-old-tag");
assertSame(tc.map(2), [["my-old-tag", 3]]);
tc.add("my-first-tag");
tc.add("my-first-tag");
assertSame(tc.map(2), [["my-old-tag", 3], ["my-first-tag", 2]]);
tc.add("my-second-tag");
assertSame(tc.map(2), [["my-old-tag", 3], ["my-first-tag", 2]]);
tc.add("my-third-tag");
assertSame(tc.map(2), [["my-first-tag", 2], ["my-third-tag", 1]]);
}
});
function assertSame(map, expected) {
var keys = map.getKeys();
assert.areEqual(expected.length, keys.length);
for(var i=0,len=expected.length; value=expected[i], i<len; i++) {
assert.areEqual(value[0], keys[i]);
assert.areEqual(value[1], map.get(value[0]));
}
};
...
[/javascript]
Im Test fügen wir nun eine Methode assertSame hinzu, um unsere Erwartungen an die map-Methode einfacher auszudrücken: Die Map sollte die erwartete Anzahl von Tags mit deren Anzahl in der richtigen Reihenfolge enthalten. Die Erwartung ist also eine Liste von [tag, count]-Tupeln. Wir können wieder die Klassen der Sammlungsbibliothek verwenden, um dies zu implementieren: Die Klasse RedBlackMultiMap implementiert eine nach Schlüsseln sortierte Map, die mehrere Werte für denselben Schlüssel zulässt.
[javascript]
TagCounter.prototype.map = function (opt_size) {
var orderedMap = new js_cols.RedBlackMultiMap(function (a,b) {return b-a;});
this.tagCounts.forEach(orderedMap.insert, orderedMap);
var result = new js_cols.LinkedHashMap();
if (opt_size) {
var i = 0;
orderedMap.forEach(function (value, key) {
if (i < opt_size) {
result.insert(value, key);
}
i++;
});
} else {
orderedMap.forEach(result.insert, result);
}
return result;
};
[/javascript]
In Zeile 2 wird der RedBlackMultiMap-Konstruktor mit einer Comparator-Funktion verwendet, die die natürliche Reihenfolge der Zahlen umkehrt. Auf diese Weise sortieren wir in absteigender Reihenfolge. In Zeile 3 werden alle Elemente der tagCount-Map in die orderedMap eingefügt, wobei die Schlüssel-Wert-Beziehung umgekehrt wird. Die Tatsache, dass die forEach-Methode eine Funktion übernimmt, die den Wert als erstes und den Schlüssel als zweites Argument hat (was normalerweise verwirrend wäre), erscheint nun als praktisches Feature! In den Zeilen 4 bis 7 wird die Schlüssel-Wert-Beziehung wieder umgekehrt, wobei sichergestellt wird, dass nur die obersten opt_size-Elemente aus der Map genommen werden (wenn sie gesetzt sind).
Fazit
Wenn ich mir die Einfachheit des Codes und der Tests ansehe, würde ich sagen, dass es definitiv möglich ist, gut getestete und dokumentierte JavaScript-Algorithmen und Datenstrukturen zu schreiben. Ich stimme meinem Kollegen jedoch voll und ganz zu, dass man solchen Code nicht oft in einem gewöhnlichen Webanwendungsprojekt findet. Vielleicht ist es an der Zeit, den JavaScript-Code in unseren Projekten mehr wie Java-Code zu behandeln und dafür zu sorgen, dass er richtig strukturiert und getestet ist.
Verfasst von
Maarten Winkels
Contact



