Part 5 – Testing

5. September 2013

Im vorherigen Artikel haben wir unsere Anwendung mit AngularJS fertiggestellt. Im aktuellen Artikel geht es darum, wie man seine Anwendung durch Tests auch für eine zukünftige Anpassung fit hält oder vor unbedarften Änderungen schützt. Dafür möchte ich zwei Frameworks nutzen, die von AngularJS empfohlen werden. Karma und Jasmine. Während Karma nur die ausführende Umgebung ist, ist Jasmine ein BDD Testing Framework.

 

Karma

Karma ist ein NodeJS Module und läuft in einer Konsole. Wird Karma gestartet, werden alle Tests ausgeführt. Zunächst einmal nichts Außergewöhnliches. Der Vorteil besteht nun darin, dass beim Ändern der Quellcodedateien sofort alle Tests wieder ausgeführt werden. Man erhält also direkten Feedback ob Codeänderungen dazu führen, dass bestehender Code immer noch oder nicht mehr funktioniert. Natürlich muss die Konsole dafür offen bleiben 😉

Karma führt alle Tests in einem Browser durch. Dadurch kann sichergestellt werden, dass der Code auch wirklich im Browser funktioniert.

Karma kann auch in diverse Continuous Integration Systeme wie Jenkins, Travis und Teamcity eingebunden werden.

Als Testing Framework kann in Karma neben Jasmine (wird von AngularJS präferiert) auch noch Mocha und QUnit benutzt werden.

Praxis

Damit Karma funktioniert, muss es konfiguriert werden. Wenn man sich das angular-seed Projekt heruntergeladen hat, befindet sich dort schon eine karma.conf im config Ordner und eine test.bat im test Ordner. Führt man die test.bat aus, öffnet sich die Konsole und Karma startet den Browser. Der Browser wird in der karma.conf angegeben. Im angular-seed Projekt ist dort standardmäßig Chrome eingetragen. Der Browser darf minimiert, aber nicht geschlossen werden. Das Gleiche gilt für die Konsole. Wobei diese auch nicht minimiert werden sollte, um den direkten Feedback auch zu sehen.

Die Konfiguration sieht folgendermaßen aus:

   1: basePath = '../';

   2:  

   3: files = [

   4:   JASMINE,

   5:   JASMINE_ADAPTER,

   6:   'app/lib/angular/angular.js',

   7:   'app/lib/angular/angular-*.js',

   8:   'test/lib/angular/angular-mocks.js',

   9:   'app/js/**/*.js',

  10:   'test/unit/**/*.js'

  11: ];

  12:  

  13: autoWatch = true;

  14:  

  15: browsers = ['Chrome'];

  16:  

  17: junitReporter = {

  18:   outputFile: 'test_out/unit.xml',

  19:   suite: 'unit'

  20: };

Aber nun genug der Theorie. Testen wir unseren TodoController (test/unit/controllerSpec.js):

   1: describe('Test Controllers', function(){

   2:     beforeEach(function(){

   3:         this.addMatchers({

   4:             toEqualData: function(expected) {

   5:                 return angular.equals(this.actual, expected);

   6:             }

   7:         });

   8:         angular.mock.module('todoApp.controllers');

   9:     });

  10:     describe('TodoController', function(){

  11:         var scope, ctrl, serviceMock;

  12:         beforeEach(inject(function($rootScope, $controller) {

  13:             var data = [{description: 'dies'}, {description: 'das'}];

  14:  

  15:             serviceMock = {

  16:                 get: function(){

  17:                     return data;

  18:                 },

  19:                 add: function(description){

  20:                     data.push({description: description});

  21:                 }

  22:             };

  23:  

  24:             scope = $rootScope.$new();

  25:             $controller('TodoController', {

  26:                         $scope: scope,

  27:                         todoService: serviceMock

  28:                     });

  29:         }));

  30:  

  31:  

  32:         it('should exist two todo items in the todos', function() {

  33:             expect(scope.todos).toEqualData(

  34:               [{description: 'dies'}, {description: 'das'}]);

  35:         });

  36:  

  37:         

  38:         it('should add one new todo entry to the todos Array and clear newTodo Text', function() {

  39:             scope.newTodo = "Woop";

  40:             scope.addNew();

  41:             expect(scope.todos).toEqualData(

  42:                 [{description: 'dies'}, 

  43:                 {description: 'das'}, 

  44:                 { description: 'Woop'}]);

  45:             expect(scope.newTodo).toBe("");

  46:         });   

  47:     });

  48:     describe("AboutController", function(){

  49:         var scope;

  50:         beforeEach(inject(function($rootScope, $controller){

  51:             scope = $rootScope.$new();

  52:             $controller('AboutController', {$scope: scope});

  53:         }));

  54:  

  55:         it('should be "Kleine Anwendung zum verwalten von Todos" ',function(){

  56:             expect(scope.aboutMessage).toBe("Kleine Anwendung zum verwalten von Todos");

  57:         });

  58:     });

  59: });

Hier sehen wir, dass wir einen Test für alle Controller “beschreiben”. Das drücken wir mit describe(‘Test Controllers’,function(){…}); aus.

Für unsere Tests benötigen wir für jeden Controller eine Funktion, die zwei Arrays miteinander vergleicht. Zusätzlich muss das Modul todoApp.controllers als Mock geladen sein. Das wird durch die erste beforeEach(…) Funktion sichergestellt.

Als Nächstes kommen dann schon die Tests für die Controller. Hierfür “beschreibt” man auch wieder was getestet werden soll. Im ersten Fall ist das der TodoController, wo im Inneren die einzelnen Testfälle beschrieben werden.

Bevor allerdings die Testfälle beschrieben werden, müssen wir noch dafür sorgen, dass für jeden Testfall auch eine frische TodoController-Instanz erzeugt wird. Zum Erzeugen der Instanz wird eigentlich unser todoService benötigt. Da wir aber nicht noch eine zweite Abhängigkeit haben wollen, die den Test beeinflusst, erzeugen wir stattdessen einen serviceMock.

An dieser Stelle ist der Vorteil des modularen Aufbaus und der Dependency Injection klar zu erkennen. Wir können dadurch einfach von außen einen eigenen Service implementieren und für die Tests dem Controller injizieren.

Kommen wir nun zu unsere Testfällen.

Für den TodoController wurden zwei Tests erstellt. Der Erste überprüft, dass nach dem erzeugen des TodoController auch etwas in den todos eingestellt ist.

Der Zweite Test überprüft das Hinzufügen eines neuen Eintrags und das daraus resultierende Leeren des eingegeben Textes (Todo Beschreibung).

Speichert man alles ab, wird der Test direkt ausgeführt und sollte folgende Meldung ausgeben:

test1

Soo, nun nehmen wir mal an, wir sind ein anderer Entwickler, der keine Ahnung von unserer Anwendung hat. Dafür öffnen wir mal unsere app/js/controller.js Datei (die wir im letzten Artikel erstellt haben), gehen zu der TodoController.addNew Funktion und entfernen scope.newTodo = “”;.

Augenblicklich wird in der Konsole folgendes zu sehen sein:

test2

Es wird einem also sofort auf die Finger gehauen, wenn Codeänderungen gemacht werden, die zunächst mal so nicht gedacht sind.

Neben Unit Tests, kann man auch noch End-To-End Tests verwenden um z.B. sicherzustellen, dass die Navigation von SeiteA zu SeiteB auch wirklich funktioniert. Oder, dass beim Drücken eines Buttons auch wirklich das div mit der id=”searchResult” sichtbar geschaltet wird. Allerdings muss man den E2E-Test immer manuell ausführen.

Fazit

Wir sind nun am Ende der Serie angekommen. Zeit also für ein kleines Fazit.

Wir haben in den letzten Artikeln das JavaScript-Framework knockoutJS und AngularJS betrachtet und festgestellt, dass die Frameworks vieles für den Entwickler übernehmen, das früher als Negativpunkt für JavaScript gesehen wurde. Dadurch ist die Entwicklung mit JavaScript viel viel angenehmer geworden. Dieser Trend ist aber nicht erst seit diesen beiden Frameworks der Fall. Auch andere Frameworks erleichtern auf ihre Weise die Arbeit mit JavaScript. Die einen durch das Einführen von Patterns, die anderen durch das Anbieten von fertigen UI- und Layout-Komponenten.

Wir haben gesehen, dass auch bekannte Entwicklungspatterns wie MVVM oder MVC, unter anderem durch die betrachteten Frameworks, unterstützt werden und JavaScript somit nicht nur für Webentwickler interessanter geworden ist, sondern auch Desktopentwickler, die z.B. aus der WPF-Entwicklung kommen, ihren Spaß damit haben werden.

Wir haben gesehen, dass durch Testing Frameworks wie Karma, Jasmine, Mocha oder QUnit die JavaScript Entwicklung qualitativ hochwertiger und robuster wird.

Ich hoffe, es ist dadurch deutlich geworden, welches Potential in der Sprache steckt und das in Zukunft nicht nur im Web damit entwickelt wird 🙂