-
Notifications
You must be signed in to change notification settings - Fork 0
/
javascript-testing.tex
855 lines (642 loc) · 28.8 KB
/
javascript-testing.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
% !TEX TS-program = xelatex
% !TEX encoding = UTF-8
\documentclass[11pt, a4paper]{article}
\usepackage{xltxtra} % loads fontspec and xunicode
\defaultfontfeatures{Mapping = tex-text, Scale = MatchLowercase}
\setmainfont{Adobe Garamond Pro}
\setsansfont{Helvetica}
\setmonofont{Bitstream Vera Sans Mono}
\usepackage{polyglossia}
% \setdefaultlanguage{austrian}
\setdefaultlanguage{german}
\usepackage{graphicx}
\usepackage{minted}
\definecolor{minted-bg}{rgb}{0.95, 0.95, 0.95}
\setlength\parindent{0mm}
\setlength\parskip{2mm}
\usepackage{url}
\usepackage{color}
\usepackage[pdfborder={0 0 0 0}]{hyperref}
% \usepackage{titlesec}
\usepackage{sectsty}
\sectionfont{\sffamily\LARGE}
\subsectionfont{\sffamily\Large}
\begin{document}
\begin{titlepage}
\begin{center}
\textsf{\textbf{\Huge Unit-Tests für JavaScript und AJAX}}\\[15cm]
\end{center}
\begin{flushright}
\textsf{\Large Peter Krenn}\\
\textsf{\href{mailto:[email protected]}{[email protected]}}\\
\textsf{0126166}\\
\textsf{066 534}\\[1cm]
\textsf{Seminar aus Multimedia}\\
\textsf{188 297}\\[1cm]
\textsf{10. Jänner 2011}
\end{flushright}
\end{titlepage}
\begin{abstract}
Durch den Einsatz von JavaScript in komplexer werdenden Web- und
Server-Anwendungen bei einer größer werdenden Anzahl von JavaScript
Implementierungen gewinnt JavaScript Testing an Bedeutung. Dieser Artikel stellt
aktuelle Methoden zu Unit Testing, Behaviour Driven Development,
Mocking/Stubbing und Regression Testing mit JavaScript vor.
\end{abstract}
\addvspace{5cm}
\tableofcontents
\clearpage
\section{Einleitung}
Laut einer Umfrage\cite{young_dailyjs_2010} testen nur 34\% der Entwickler ihren
JavaScript Code. Im Vergleich zur vorjährigen Version der
Umfrage\cite{young_dailyjs_2009} gab es nur eine Verbesserung um 2\%. Trotzdem
existiert eine Vielzahl von JavaScript Testing Frameworks.
Die verbreitetsten Open Source JavaScript Frameworks und Libraries, wie zum
Beispiel jQuery\cite{resig_jquery_2011},
Prototype/Scriptaculous\cite{prototype_js_prototype_2011},
MooTools\cite{mootools_mootools_2011} oder YUI\cite{yahoo_inc._yui_2011-1}
verfügen alle über umfassende Test Suites.
Die von den JavaScript Frameworks verwendeten Test Frameworks sind auch die
am meisten eingesetzten: QUnit\cite{zaefferer_qunit_2011} (jQuery) 34\%,
Jasmine\cite{jasmine_jasmine_2011} (MooTools) 18\%,
YUITest\cite{yahoo_inc._yui_2011} (YUI) 6\%,
unittest.js\cite{fuchs_unittest.js_2011} (Prototype) 5\%.
JsUnit\cite{jsunit_jsunit_2011} ist weiterhin mit 12\% vertreten. Dieses
Framework war das erste JavaScript Unit Testing Framework und die Entwicklung
daran begann 2001, wurde aber mittlerweile eingestellt.
Neben browser-basiertem Testen gewinnt server-seitiges Testen an Bedeutung,
bereits 35\% der Entwickler verwenden JavaScript in einer Server Umgebung.
Node.js beispielsweise beinhaltet eine Implementierung der CommonJS Assertion Testing
Spezifikation\cite{node.js_node.js_2011}.
Ebenfalls für server-seitiges Testen relevant sind Headless Testing Frameworks,
wie zum Beispiel Jasmine. Sie sind nicht vom DOM-Modell des Browsers oder
anderen Frameworks abhängig und können auch von der Kommandozeile aus aufgerufen
werden.
Auch Mocking und Stubbing Libraries wurden für JavaScript entwickelt.
Zum Beispiel Sinon.JS\cite{johansen_sinon.js_2011} funktioniert mit jedem Unit
Testing Framework und erlaubt es, verschiedene Arten von Test Doubles und Fake
XHR Requests zu erzeugen.
Der weitere Artikel beschäftigt sich vor Allem mit der Anwendung von QUnit und
Jasmine. Diese beiden Test Frameworks sind die, an denen zur Zeit am
aktivsten entwickelt wird.
\clearpage
\section{QUnit}
QUnit\cite{zaefferer_qunit_2011} wird von Mitgliedern des jQuery
Teams\cite{resig_jquery_2011} entwickelt und ist auch die offizielle
\emph{test suite} des jQuery Projektes. Es zählt zur Gruppe der
xUnit Test Frameworks\cite{fowler_xunit_2010}, die einem Design folgen,
das erstmals mit SUnit\cite{beck_simple_1994} für Smalltalk formuliert
wurde und als JUnit\cite{gamma_junit_2011} für Java große Verbreitung fand.
\subsection{Assertions}
Der zentrale Bestandteil eines \emph{unit tests} ist die Assertion. Eine
Assertion ist eine Aussage über den Zustand einer bestimmten Stelle in
einem Programm\cite{wikipedia_assertion_2011}. Der Entwickler macht eine
Zusicherung, dass eine bestimmte Bedingung unabhänging von den Laufzeitumständen
immer wahr ist.
QUnit stellt eine Reihe von allgemeinen Assertions zur Verfügung,
die JUnit nachempfunden sind, und auch solche, die speziell an JavaScript
und asynchrone Entwicklung angepasst sind\cite{zaefferer_qunit_2011}:
\subsubsection*{\texttt{ok(state, message)}}
\texttt{ok()} ist eine boolsche Assertion, die positiv terminiert, wenn der
erste Parameter wahr ist. Den zweiten, optionalen Parameter \texttt{message} haben alle
Assertion-Funktionen gemein: dieser wird gemeinsam mit dem Ergebnis
ausgegeben.
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
ok(true, "terminiert immer positiv");
ok("", "Leerstring entspricht false, die assertion schlägt fehl");
\end{minted}
\subsubsection*{\texttt{equal(actual, expected, message)}}
Diese Assertion ist \texttt{ok()} sehr ähnlich, allerdings werden die
\texttt{actual} und \texttt{expected} Werte mit dem Ergebnis ausgegeben. Dadurch
werden die Tests aussagekräftiger und Debugging einfacher. \texttt{equal()}
terminiert positiv, wenn \texttt{actual == expected}.
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
var actual = 1;
equal(actual + 1, 2, "terminiert positiv");
equal(actual, 2, "schlägt fehl");
\end{minted}
\clearpage
\subsubsection*{\texttt{strictEqual(actual, expected, message)}}
Im Gegensatz zu \texttt{equal()} werden bei dieser Assertion die Werte
mit \texttt{actual === expected} verglichen. Dieser Operator vergleicht
zusätzlich die Typengleichheit der beiden Parameter.
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
var actual = 0;
// terminiert positiv, da == den Datentyp ignoriert
equal(actual, false);
// schlägt fehl, da 0 vom Typ Number und false Boolean ist
strictEqual(actual, false);
\end{minted}
\subsubsection*{\texttt{deepEqual(actual, expected, message)}}
\texttt{deepEqual()} überprüft primitive Datentypen, Arrays und Objekte
rekursiv auf Gleichheit. Sie ist dabei auch strikter als \texttt{equal()}, da
der \texttt{===} Operator verwendet wird.
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
var actual = {a: 1};
// equal schlägt mit verschiedenen Objekten fehl
equal(actual, {a: 1});
// schlägt fehl, da die Datentypen verschieden sind;
deepEqual(actual, {a: "1"});
// terminiert positiv, da die Inhalte der Objekte gleich sind
deepEqual(actual, {a: 1}
\end{minted}
\subsubsection*{\texttt{notEqual(actual, expected, message)}}
Diese Assertion ist die invertierte Version von \texttt{equal()}.
Entsprechende Funktionen existieren auch für \texttt{strictEqual()} und
\texttt{deepEqual()}: \texttt{notStrictEqual()} und \texttt{notDeepEqual()}.
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
var actual = 0;
notEqual(actual, false, "schlägt fehl");
notStrictEqual(actual, false, "terminiert positiv");
var actual = {a: 1};
notDeepEqual(actual, {a: "1"}, "terminiert positiv");
\end{minted}
\subsubsection*{\texttt{raises(state, message)}}
\texttt{raises()} überprüft, ob die übergebene Callback Funktion eine Exception
wirft. Die Funktion wird ohne Parameter im \emph{default scope} aufgerufen.
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
raises(function() {
throw new Error("Fehlerfall");
}, "terminiert positiv, falls der gewünschte Fehler auftritt");
\end{minted}
\subsection{Testfunktionen}
Werden die vorgestellten Assertions direkt ausgeführt, unterbrechen sie
im Falle eines Fehlers den Testdurchlauf. Deswegen werden sie in Testfunktionen
eingebettet.
Es ist auch üblich, dass jede Testfunktion nur eine spezifische Eigenschaft
testet. Dies geschieht oft mit mehreren Assertions und die Testfälle
bleiben dabei überschaubar und einfach zu verstehen.
QUnit macht das mit der Funktion \texttt{test(name, expected, test)}.
\texttt{name} ist eine Bezeichnung des Tests und \texttt{test} ist eine
Funktion, die die Assertions enthält. \texttt{expect} ist optional und
erlaubt es die Anzahl der zu erwartenden Assertions festzulegen. Dies ist
bei asynchronen Tests von Bedeutung.
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
test("Calculator#add()", function() {
equal(Calculator.add(3, 4), 7, "two positive values");
equal(Calculator.add(-3, 4), 7, "with a negative value");
});
\end{minted}
\clearpage
\subsection{Test Suites}
Mit der \texttt{module(name, lifecycle)} Anweisung ist es möglich, die Testfälle
weiter in Module aufzuteilen. Dies dient einerseits der Strukturierung der Tests
und andererseits wird es auch möglich, für mehrere Testfälle relevante Daten vor
deren Ausführung zu definieren.
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
module("Calculator", {
setup: function() {
this.a = 3;
this.b = 4;
}
});
test("add()", function() {
equal(Calculator.add(this.a, this.b), 7);
});
\end{minted}
Im Parameter \texttt{lifecycle} kann man optional die Funktionen
\texttt{setup()} und \texttt{tearDown()} übergeben, die vor beziehungsweise nach
jedem Test ausgeführt werden. Sie teilen den \texttt{this}-\emph{scope} der
Testfunktionen, wodurch sogenannte \emph{fixtures} erstellt werden können. Die
Daten werden nach jedem Test zurückgesetzt, was in \texttt{tearDown()} auch
manuell geschehen kann.
\subsection{Asynchrones Testen}
Asynchrone Funktionen, wie sie bei AJAX Requests und Aufrufen von
\texttt{setTimeout()} und \texttt{setInterval()} vorkommen, können mit den
bisher vorgestellten Methoden nicht getestet werden.
Im folgenden Beispiel\cite{huang_how_2010} wird die Assertion nicht ausgeführt, da das
Ergebnis des Testfalles zum Zeitpunkt des Aufrufens bereits feststeht.
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
test("Asynchroner Test", function() {
setTimeout(function() {
ok(true);
}, 100)
});
\end{minted}
QUnit stellt aus diesem Grund zwei Funktionen zur Verfügung, mit der
sich der Testablauf pausieren und fortsetzen lässt.
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
test("Asynchroner Test", function() {
stop();
setTimeout(function() {
ok(true);
start();
}, 100)
})
\end{minted}
\texttt{stop()} hält den Test an und nach Aufrufen der Assertion, wird
mit \texttt{start()} wieder fortgesetzt. Da der Fall, dass \texttt{stop()} am
Anfang eines Tests ausgeführt wird, häufig vorkommt, gibt es dafür auch ein
spezielle Funktion namens \texttt{asyncTest()}.
Wird nicht ein \texttt{setTimeout()} Aufruf sondern ein AJAX Request
getestet, bei dem man sich nicht sicher sein kann, wann und ob die
\emph{callback} Funktion ausgeführt wird, ist es möglich der \texttt{stop()}
Funktion ein Intervall als Parameter mitzugeben, nachdem der Testablauf
automatisch fortgesetzt wird. Der betreffende Test schlägt dabei fehl.
Werden in einem Testfall mehrere zeitverzögerte Aufrufe getestet, ist es
notwendig, \texttt{start()} manuell mit \texttt{setTimout()} ausreichend zu
verzögern, so dass alle Callbacks ausreichend Zeit zum Terminieren haben.
Um sicher zu stellen, dass alle Callbacks mit den enthaltenen
Assertions ausgeführt werden, gibt es eine \texttt{expect()} Funktion,
der man als Parameter die zu erwartende Anzahl der Assertions übergeben
kann.
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
// Ein AJAX request mit der zu testenden callback Funktion
function ajaxCall(successCallback) {
jQuery.ajax({
url: "http://server.com/",
success: successCallback
});
}
asyncTest("Asynchroner Test", function() {
// asyncTest unterbricht den Testablauf
// 3 assertions sollten ausgeführt werden
expect(3);
ajaxCall(function() {
ok(true);
});
ajaxCall(function() {
ok(true);
ok(true);
})
// Nach 3000 ms wird der Testablauf fortgesetzt
setTimeout(function() {
start();
}, 3000);
});
\end{minted}
Die Anzahl der Assertions kann auch direkt im \texttt{asyncTest(name, expected,
test)} Aufruf definiert werden.
\clearpage
\subsection{Testablauf}
Die Ausgabe von QUnit erfolgt ausschließlich im Browser. Die Da\-tei\-en
\texttt{qunit.js} und \texttt{qunit.css} müssen gemeinsam mit dem zu testenden
JavaScript Code und den Tests in eine HTML-Datei eingebunden werden.
\begin{minted}[gobble = 2, bgcolor = minted-bg]{html}
<!DOCTYPE html>
<html lang="en">
<head>
<script type="text/javascript" src="qunit.js"></script>
<link rel="stylesheet" href="qunit.css" type="text/css"
media="screen" />
<!-- Projekt Datei -->
<script type="text/javascript" src="my_project.js"></script>
<!-- Datei mit den Tests -->
<script type="text/javascript" src="my_tests.js"></script>
</head>
<body>
<h1 id="qunit-header">QUnit example</h1>
<h2 id="qunit-banner"></h2>
<div id="qunit-testrunner-toolbar"></div>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests"></ol>
<div id="qunit-fixture">test markup, will be hidden</div>
</body>
</html>
\end{minted}
Beim Ausführen der Datei im Browser befüllt QUnit die Elemente im
\texttt{body} mit den Ergebnissen der Tests.
\begin{center}
\includegraphics[width = 1\textwidth]{QUnit.png}
\end{center}
Für jeden Testfall wird die Anzahl der erfolgreichen und fehlgeschlagenen
Assertions und auch deren Gesamtanzahl angegeben. Für fehlgeschlagene
Tests wird dessen Position im Quellcode angezeigt.
QUnit ist nur für die Verwendung im Browser ausgelegt, ein
kommandozeilen-basierter Aufruf der Testfälle ist nicht vorgesehen. Ein
Versuch\cite{plee_qunit_2010} QUnit in der simulierten Browserumgebung
Env.js\cite{resig_envjs_2011} anzuwenden, war nur begrenzt erfolgreich, da der
Testbericht im HTML-Format ausgegeben wird. Eine Alternative ist das weiter
unten beschriebene TestSwarm Projekt.
\section{Jasmine}
Jasmine\cite{jasmine_jasmine_2011} gehört zur Gruppe der Behavior Driven
Development\cite{wikipedia_behavior_2011} oder BDD Frameworks. Im Gegensatz zu
traditionellen xUnit Test Frameworks versuchen diese die Sprache, in der die
Tests verfasst sind, einfacher und damit auch für nicht-technische
Projektteilnehmer verständlich zu machen. BDD soll dabei helfen, die gewünschte
\emph{software behavior} in Diskussion mit den Auftraggebern klar zu definieren.
Im Gegensatz zu seinem Vorgänger Screw.Unit\cite{sobo_screw.unit_2011} und QUnit
ist Jasmine ein Headless Testing Framework, das heißt es benötigt keinen
Browser und ist nicht von anderen JavaScript Libraries abhängig. Dies
vereinfacht den automatisierten Aufruf der Tests und ermöglicht es server-side
Anwendungen zu testen. Wie QUnit unterstützt es asynchrones Testen und verfügt
außerdem über Mocking und Stubbing Funktionalitäten.
\subsection{Specs, Expectations und Suites}
Specs entsprechen den Testfunktionen von klassischen xUnit Test Frameworks. Sie
beschreiben das Verhalten einer Funktion oder eines Objektes mit einem Aufruf
der Funktion \texttt{it()} in Form einer natürlichen Sprache. Die Beschreibung
sollte aussagekräftig sein, da sie im Testbericht ausgegeben wird.
Expectations drücken die Erwartungen an das Verhalten eines bestimmten Teils des
Quellcodes mittels der \texttt{expect()} Funktion aus. Sie sind vergleichbar mit
den Assertions der xUnit Frameworks. Das gewünschte Verhalten wird durch
Matchers wie \texttt{toEqual} spezifiziert:
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
it("should increment a variable", function() {
// Definition der Testdaten
var value = 0;
// Ausführen des zu testenden codes
value++;
// Testen der Erwartung
expect(value).toEqual(1);
});
\end{minted}
Zur Strukturierung der Specs dienen Suites. Wie bei QUnit ist es möglich,
Anweisungen vor und nach dem Aufrufen jedes Tests auszuführen -- dies geschieht
mit \texttt{beforeEach()} und \texttt{afterEach()}.
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
describe("Calculator", function() {
var a, b;
beforeEach(function() {
a = 3;
b = 4;
});
it("should add numbers with add()", function() {
expect(Calculator.add(a, b)).toEqual(7);
});
it("should multiply numbers with multiply()", function() {
expect(Calculator.multiply(a, b)).toEqual(12);
});
});
\end{minted}
\clearpage
\subsection{Matchers}
Jasmine verfügt über eine Reihe von eingebauten Matchers und deren Inversionen,
die man durch Voranstellen von \texttt{.not} erhält:
\subsubsection*{\texttt{toEqual()}}
Dieser Matcher entspricht der QUnit Assertion \texttt{deepEqual()} und
kann außerdem Datumswerte und DOM-Nodes vergleichen.
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
// Alle expectations terminieren positiv
expect(true).toEqual(true);
expect(false).not.toEqual(true);
expect("5").not.toEqual(5);
expect({a: 1}).not.toEqual({a: "1"});
expect(new Number("5")).toEqual(5);
expect(new Date("January 11, 2011, 12:00:00"))
.not.toEqual(new Date("January 12, 2011, 12:30:00"));
var nodeA = document.createElement("div");
var nodeB = document.createElement("div");
expect(nodeA).toEqual(nodeA);
expect(nodeA).not.toEqual(nodeB);
\end{minted}
\subsubsection*{\texttt{toBe()}}
\texttt{toBe()} entspricht \texttt{strictEqual()}.
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
var a = {};
var b = {};
var c = b;
expect(a).not.toBe(b);
expect(c).toBe(b);
\end{minted}
\clearpage
\subsubsection*{\texttt{toMatch()}}
\texttt{toMatch()} überprüft Strings auf Übereinstimmungen mit Regulären
Ausdrücken.
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
expect("javascript").toMatch(/script/);
expect("javascript").not.toMatch(/testing/);
\end{minted}
\subsubsection*{\texttt{toBeDefined()}}
\texttt{toBeDefined()} terminiert positiv für definierte Werte.
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
var a = 5;
expect(a).toBeDefined();
expect(undefined).not.toBeDefined();
\end{minted}
\subsubsection*{\texttt{toBeNull()}}
\texttt{toBeNull()} erwartet einen \texttt{null}-Wert.
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
expect(null).toBeNull();
expect(undefined).not.toBeNull();
\end{minted}
\subsubsection*{\texttt{toBeTruthy()} und \texttt{toBeFalsy()}}
Diese Matchers überprüfen, ob ein Ausdruck in \texttt{true} beziehungsweise
\texttt{false} evaluiert.
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
expect(0).not.toBeTruthy();
expect("").toBeFalsy();
expect(5).toBeTruthy();
expect({a: 1}).not.toBeFalsy();
\end{minted}
\clearpage
\subsubsection*{\texttt{toContain()}}
\texttt{toContain()} terminiert positiv, falls der übergebene String oder Array
ein bestimmtes Zeichen oder Element enthält.
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
expect("javascript").toContain("a");
expect("javascript").not.toContain("x");
expect([1, 2, 3]).toContain(3);
expect([1, 2, 3]).not.toContain("x");
\end{minted}
\subsubsection*{\texttt{toBeLessThan()} und \texttt{toBeGreaterThan()}}
Diese Matchers überprüfen, ob ein Wert kleiner beziehungsweise größer als der
Übergebene ist.
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
expect(5).toBeLessThan(10);
expect(5).not.toBeGreaterThan(10);
\end{minted}
\subsubsection*{\texttt{toThrow()}}
Mit \texttt{toThrow()} drückt man die Erwartung aus, dass ein festgelegter
Ausdruck die übergebene Exception wirft.
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
expect(function() { throw new Error("error"); }).toThrow("error");
\end{minted}
\subsubsection*{Benutzerdefinierte Matchers}
Um die Lesbarkeit der Specs zu verbessern, ist es mit Jasmine auch möglich eigene
Matchers zu definieren. Eine Matcher Funktion erhält den zu vergleichenden Wert
als \texttt{this.actual} und es können zusätzlich beliebig viele Parameter
übergeben werden. Die benutzerdefinierten Matchers werden mit
\texttt{this.addMatchers()} in der \texttt{beforeEach()} oder der \texttt{it()}
Funktion definiert.
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
beforeEach(function() {
this.addMatchers({
toBeLessOrEqualThan: function(expected) {
return this.actual <= expected;
}
});
});
\end{minted}
\subsection{Asynchrones Testen}
Zum Testen von asynchronen Funktionen stellt Jasmine die Funktionen
\texttt{runs()}, \texttt{waits()} und \texttt{waitsFor()} bereit.
Von \texttt{runs()} aufgerufene Funktionen für sich alleine verhalten sich
genauso, als wären sie direkt aufgerufen worden. In Verbindung mit
\texttt{waits()} kann der Testablauf für eine bestimmte Zeit unterbrochen
werden, bis die nächste \texttt{runs()} Funktion aufgerufen wird. Die
\texttt{runs()} Blöcke innerhalb eines \texttt{it()} Blocks teilen den gleichen
\texttt{this}-\emph{scope}.
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
it("should increment a value after 250 ms", function() {
runs(function() {
this.value = 0;
// 250 ms in der Zukunft wird value erhöht
setTimeout(function() {
this.value++;
}, 250);
});
// value entspricht noch dem initialisierten Wert
runs(function() {
expect(this.value).toEqual(0);
});
waits(500);
// 500 ms später wurde value korrekt erhöht
runs(function() {
expect(this.value).toEqual(1);
});
});
\end{minted}
\clearpage
\texttt{waitsFor()} pausiert den Testablauf, bis ein bestimmtes Ereignis
eintrifft. Als erster Parameter wird eine Funktion übergeben, die \texttt{true}
im Falle des Eintreffens des Ereignisses zurück gibt. Optional kann auch die
maximale Wartedauer und eine Fehlermeldung übergeben werden.
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
describe('Calculation', function() {
it('should calculate a value asynchronously', function() {
var calculation = new Calculation();
calculation.asynchronouslyCalculateValue();
// waitsFor() wartet bis isComplete() true zurück gibt
// maximal aber eine Sekunde
waitsFor(function() {
return calculation.isComplete();
}, "calculation wasn't completed", 1000);
runs(function() {
expect(calculation.value).toEqual(1);
});
});
});
\end{minted}
\subsection{Spies}
Jasmine Spies sind Test Doubles\cite{meszaros_test_2011-1}, die als Stubs,
Spies oder in Verbindung mit einer Expectation als Mocks agieren können.
Test Doubles sind Objekte, die das Interface oder Verhalten eines realen
Objektes simulieren und werden eingesetzt um die Komplexität der Tests zu
reduzieren. Bei Projekten, die viele verschachtelte Objekte und Module umfassen,
macht es Sinn, sich beim Testen nur auf relevante Teile zu konzentrieren und bei
Kommunikation nur auf die korrekte Verwendung der Interfaces zu achten. Für
JavaScript relevante Beispiele sind asynchrone Aufrufe oder AJAX Requests.
Test Stubs sind Objekte, die ausschließlich auf die für den Test notwendigen
Aufrufe reagieren. Eine erweiterte Version sind Test Spies, sie zeichnen die
Anzahl und Parameter der Aufrufe auf, welche später in Assertions oder
Expectations getestet werden können. Mocks sind vordefinierte Objekte mit
eingebauten Expectations, die bestimmen welche Aufrufe erwartet werden.
\texttt{spyOn(object, methodName)} verhindert im ersten Schritt den Aufruf einer
Funktion und zeichnet folgende Eigenschaften auf: \texttt{callCount} enthält wie
oft die Funktion aufgerufen wurde, \texttt{mostRecentCall.args} ist
Parameter-Array des letzten Aufrufes und mit \texttt{argsForCall[]} kann man auf
die Parameter sämtlicher Aufrufe zugreifen.
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
// Ein Objekt, dessen Interface simuliert wird
var example = {
not: function(boolean) { return !boolean; }
};
// Ein Spy wird für example.not angelegt
spyOn(example, "not");
example.not(true);
// example.not.callCount: 1
// example.mostRecentCall.args: [true]
\end{minted}
Im Falle eines Aufrufes kann die Reaktion eines Spy festgelegt werden:
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
// ruft die originale Funktion auf
spyOn(example, "not").andCallThrough();
// gibt die übergebene Werte bei Aufruf zurück
spyOn(example, "not").andReturn(true);
// wirft bei Aufruf eine Exception
spyOn(example, "not").andThrow("error");
// ruft die übergebene Funktion auf
spyOn(example, "not").andCallFake(function() { return true; });
\end{minted}
Mit folgenden Matchers können die Spies in Expectations verwendet werden.
Dadurch verhalten sich die Funktionen wie Mock Objekte.
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
spyOn(example, "not");
example.not(true);
expect(example.not).toHaveBeenCalled();
expect(example.not).not.toHaveBeenCalledWith(false);
\end{minted}
\clearpage
Anstatt eine bestehende Funktion zu einem Spy zu machen, ist es auch manchmal
sinnvoll, mit \texttt{jasmine.createSpy()} ein Stub-artiges Objekt manuell zu erstellen.
Ein Anwendungsgebiet ist das Testen von Callbacks\cite{jasmine_jasmine_2011-1}.
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
var Klass = function() {
};
Klass.prototype.methodWithCallback = function(callback) {
return callback("example");
};
it("should spy on Klass#methodWithCallback", function() {
var callback = jasmine.createSpy();
new Klass().methodWithCallback(callback);
expect(callback).toHaveBeenCalledWith("example");
});
\end{minted}
Eine weitere Anwendung ist das Testen von AJAX Request oder asynchronen
Funktionen, die Callbacks als Parameter verlangen. Die Request beziehungsweise
die Funktion wird dabei nicht ausgeführt, sondern die Callback Funktion wird
isoliert getestet.
\begin{minted}[gobble = 2, bgcolor = minted-bg]{javascript}
var Klass.prototype.asyncMethod = function(callback) {
asyncCall(callback);
};
it("should test async call") {
spyOn(Klass, 'asyncMethod');
var callback = jasmine.createSpy();
Klass.asyncMethod(callback);
expect(callback).not.toHaveBeenCalled();
// Die an asyncMethod übergebene callback Funktion
// wird manuell getestet
Klass.asyncMethod.mostRecentCall.args[0]("example");
expect(callback).toHaveBeenCalledWith("example");
});
\end{minted}
\clearpage
\subsection{Testablauf}
Für Jasmine gibt es eine stand-alone Version, deren Funktionalität der von QUnit
entspricht. In einer HTML-Datei werden die Test-, sowie die Projektdateien
eingebunden und beim Öffnen im Browser wird sie mit den Testergebnissen befüllt.
\begin{center}
\includegraphics[width = 1\textwidth]{Jasmine.png}
\end{center}
Nicht erfüllte Expectations werden farblich markiert und der Stack für den
jeweiligen Fehler wird angezeigt. Zusätzlich ist es möglich, einzelne Specs vom
Browser aus erneut aufzurufen.
Mit dem Kommandozeilen Interface von Jasmine ist es möglich automatisiert
verschiedene Browser zu starten und die Test Suites auszuführen. Die Browser
werden von Selenium\cite{selenium_selenium_2011} geladen und geben die
Ergebnisse über die Kommandozeile aus.
Ein Beispiel für die server-seitige Anwendung von Jasmine ist
jasmine-node\cite{hevery_jasmine-node_2011}, eine Adaptierung für node.js. Die
Specs können direkt mit dem node.js Interpreter ausgeführt und ausgegeben
werden.
\section{Regression Testing}
Regression Testing\cite{wikipedia_regression_2011} versucht Probleme und
Nebenwirkungen von Modifikationen im Quellcode durch Wiederholung der Testfälle
aufzuzeigen. Ein verwandter Begriff ist Continuous
Integration\cite{wikipedia_continuous_2011}, die kontinuierliche
Qualitätskontrolle für ein Projekt erreichen will. Automatisiertes Testing ist
eines ihrer Ziele.
Da es keine einheitliche JavaScript Implementierung gibt, sondern
diese sich je nach Browser, Browserversion und Betriebssystem unterscheiden, ist
die Durchführung der Testläufe problematisch.
Ein Ansatz, der sich derzeit in Entwicklung befindet, ist
TestSwarm\cite{resig_testswarm_2011} von John Resig, dem Autor von jQuery und
QUnit. Es handelt sich dabei um ein System, das den Code eines Projektes bei
jedem \emph{commit} im Versionierungssystem automatisch auf verschiedenen
Plattformen und Browsern testet.
Das System wird von einem zentralen Server aus gesteuert, der Testaufträge an
mehrere Clients, auf denen verschiedene Browserversionen laufen, weiterleitet.
Das Ergebnis der Prozesses ist eine tabellarische Ansicht die für jeden
\emph{commit} darstellt, auf welchen Plattformen und Browsern die Test Suite
fehlerfrei angewendet worden ist.
Im Gegensatz zu vergleichbaren Systemen, wie
Selenium\cite{selenium_selenium_2011}, ist TestSwarm vom verwendeten Test
Framework unabhängig: es werden alle im Moment gebräuchlichen Frameworks
unterstützt.
\clearpage
\begin{flushleft}
\bibliography{javascript-testing}
\bibliographystyle{plain}
\end{flushleft}
\end{document}