Wenn wir eine Angular-Anwendung erstellen, halten wir uns in der Regel an die vorgeschlagene oder automatisch generierte Lösung für Unit-Tests: den Karma Test-Runner und -Server, das Jasmine Test-Framework und PhantomJS als Umgebung, in der alles ausgeführt wird. In diesem Blog-Beitrag erkläre ich, warum das ziemlich albern ist, und biete einen alternativen und leichtgewichtigen Ansatz zum Schreiben und Ausführen von Unit-Tests. Er hängt von einer bestimmten Art der Definition Ihrer Angular-Komponenten ab und ist vielleicht kein vollständiger 1:1-Ersatz, aber ich kann mit Sicherheit sagen, dass Ihre Tests damit schneller werden, der Aufwand für ihre Ausführung viel geringer ist und die Qualität der Tests verbessert wird, weil Sie sich weniger Sorgen machen müssen.
Haftungsausschluss, Hintergrund und Projektstruktur
Zunächst einige Haftungsausschlüsse:
- Im Titel steht Angular, aber es ist nicht wirklich Angular-spezifisch. Ich ziele darauf ab, weil Angular standardmäßig mit dem 'schweren' Test-Setup ausgeliefert wird.
- Es wurde mit Angular 1.x im Hinterkopf geschrieben; bei meinem Auftrag, bei dem wir dies bauen, werden wir wahrscheinlich irgendwann auf Angular 2 umsteigen. Kleine Schritte. Ich denke, dass dieser Ansatz auch für Angular 2 und wahrscheinlich auch für andere Frameworks angewendet werden kann, aber ich habe mich noch nicht damit befasst.
Zweitens, einige Hintergrundinformationen. Sie können diesen Teil überspringen, wenn Sie mit der Struktur von Angular-Anwendungen vertraut sind: Bei meiner derzeitigen Aufgabe folgen wir größtenteils dem Ansatz des John Papa AngularJS-Styleguides für die Entwicklung von Komponenten, der vorschreibt, dass Dateien und Komponenten sehr klein und definiert sein müssen, einen einzigen Zweck haben und einen Dateinamen haben, der den Inhalt beschreibt:
- foo.controller.js
- foo.view.html
- foo.service.js
- foo.component.js
- routes.js
- index.js
Die verschiedenen .js-Dateien selbst enthalten in der Regel wenig bis gar keinen Angular-spezifischen Code. Es handelt sich lediglich um Funktionen, die einige Argumente wie Dienste und Konstanten entgegennehmen, oder im Falle der Komponente um ein Konfigurationsobjekt, das auf die Vorlagendatei verweist und den Controller über reguläre ES6-Importe importiert. Die Datei index.js hält alles zusammen. Sie enthält die Service- und Komponentendateien und registriert sie in Angular. Etwa so: [javascript] import FooService from './foo.service'; import Foo from './foo.component'; angular.module('foo', []) .service('FooService', FooService) .component('Foo', Foo); [/javascript] (Beachten Sie, dass wir den Controller nicht registrieren; mit den Komponenten von Angular 1.5 brauchen Sie das nicht mehr.) Die index.js wird entweder von der index.js-Datei der Hauptanwendung oder von einer anderen Anwendung eingebunden, wenn es sich um eine gemeinsam genutzte Komponente handelt. Beim Starten der Testaufgabe startet der standardmäßige Angular Test Runner Karma, der eine .html-Datei erzeugt und einen einfachen Webbrowser startet, der wiederum Implementierungs- und Testdateien lädt und dann ausführt, normalerweise in PhantomJS, aber es kann auch in einem beliebigen Browser ausgeführt werden. Das letzte Detail - kann in jedem Browser ausgeführt werden - ist eigentlich eine gute Idee, denn so können Sie Ihren Code in einer Reihe von Browsern testen und browserspezifische Probleme frühzeitig erkennen. In der Praxis habe ich allerdings noch nicht gesehen, dass dies tatsächlich genutzt wird. Für ein Unternehmen ist es ziemlich kompliziert, einen Cluster von Rechnern (virtuell oder anderweitig) einzurichten, der in der Lage ist, mehrere Browser zu starten, in denen Tests ausgeführt werden können. Es gibt (glaube ich) Drittanbieter, die Dienste für die Ausführung von Tests anbieten, aber bei jeder nicht trivialen Anwendung überwiegen die Kosten für diese Dienste schnell den Nutzen. Ein weiteres Problem bei der Verwendung von Testläufern eines Drittanbieters sind die rechtlichen oder vermeintlichen Sicherheitsprobleme beim Hochladen von Code an einen Drittanbieter. Die Sicherheit ist in diesem Fall kein Problem, da es sich in der Regel um clientseitigen Code handelt, aber es gibt viele Unternehmen, die strenge Regeln für diese Art von Dingen haben. In der Regel handelt es sich dabei um dieselbe Art von Unternehmen, die es nicht zulassen, dass Anwendungen bei Cloud-Anbietern bereitgestellt werden. Kein Urteil, nur etwas, das Sie im Hinterkopf behalten sollten.
Sie brauchen vielleicht kein Karma
Abgesehen von Komplexitäts-, Kosten-, Rechts- und Leistungsgründen müssen und sollten Sie Ihre Unit-Tests heutzutage nicht mehr in mehreren Browsern ausführen. Aus zwei Gründen:
- Wenn Sie browser-spezifisches JS schreiben, machen Sie es falsch. Oder Sie schreiben eine Bibliothek. In diesem Fall sollten Sie diesen Blogbeitrag ignorieren. Bibliotheken und Tools wie Babel, TSC und entsprechende Polyfills sollten die meisten, wenn nicht sogar alle browserspezifischen Probleme beseitigen. Zweitens: Wir schreiben das Jahr 2016, die Nutzung älterer Browser ist auf ein unbedeutendes Maß gesunken und die meisten Browser unterstützen die meisten Funktionen, die Tools wie Babel kompilieren, ohne dass Polyfills erforderlich sind. Es sollte für jede moderne Webanwendung kein Problem sein, unter IE 9 und neuer zu laufen.
- Unit-Tests sollten Logik testen, nicht Browser-Macken. Ein Unit-Test sollte sagen: "Wenn ich diese Funktion mit diesen Argumenten aufrufe, erwarte ich, dass dies passiert". Da ist nichts Browserspezifisches drin. Verschwenden Sie keine Zeit auf Dinge, die wahrscheinlich nicht passieren werden. Es sei denn, Sie schreiben eine Bibliothek oder sind clever. In diesem Fall sollten Sie aufhören, clever zu sein.
TL;DR: Meiner Meinung nach brauchen Sie die Komplexität der Ausführung in mehreren Browsern nicht, und daher brauchen Sie auch nicht den Overhead, der durch die Bereitstellung von Dateien und die Ausführung Ihrer Tests in einem Headless-Browser entsteht.
Ein anderer Ansatz für die Durchführung von Tests
Was ich vorschlage, ist einfach: Führen Sie Ihre Tests in einer einfachen NodeJS-Umgebung aus. Node startet schnell, führt Ihre JS- und Unit-Tests genauso gut aus wie PhantomJS und wenn Ihre Anwendung richtig strukturiert ist, können Sie Ihre Anwendungslogik völlig unabhängig vom Dependency-Injection-System von AngularJS testen, das eine weitere Quelle für Laufzeit-Overhead und mentalen Overhead ist. Wenn Sie irgendetwas schreiben müssen, das nicht direkt mit der jeweiligen Komponente zu tun hat, handelt es sich um Boilerplate und mentalen Overhead (das gilt übrigens auch für den Strukturcode und die Matcher einer Testbibliothek). Ein weiterer Vorteil ist, dass bei diesem Ansatz nicht alle möglichen Abhängigkeiten und Module geladen werden müssen und dass wir Dateien isoliert testen können, anstatt die gesamte Anwendung zu erstellen, zu assemblieren und zu laden.
Einrichten des Testläufers
Um dies zu ermöglichen, müssen wir nur ein paar Dinge tun:
- Installieren Sie babel-cli - leider unterstützt NodeJS immer noch keinen Import, so dass wir dafür einen Transpilierungsschritt benötigen. Möglicherweise gibt es eine abgespeckte Version der Importunterstützung, bei der nicht die gesamte Anwendung transpiliert wird. Node 6 und neuere Versionen sollten in der Lage sein, den meisten, wenn nicht sogar den gesamten ES6-Code nativ auszuführen.
- Richten Sie eine Jasmine-Konfiguration und einen Test-Runner ein. Sie können dies durch Ihr bevorzugtes Testframework und Dienstprogramme ersetzen, wenn Sie möchten.
- ?????
- Gewinn
Wir lassen diesen neuen Test-Runner derzeit parallel zu unseren bestehenden Tests laufen. Eine vollständige Neufassung kostet viel Zeit und Mühe, ist langweilig und wir müssen am Ende des Tages immer noch Funktionen entwickeln. Zweitens konzentriert sich dieser Läufer ausschließlich auf "logischen" Code - Controller, Dienste usw. Ich habe noch nicht herausgefunden, wie ich ihn auf das Testen von Direktiven oder Ansichten anwenden kann, die ebenfalls getestet werden können. Hier ist also die neue Konfiguration: [javascript] // jasmine.json { "spec_dir": "", "spec_files": [ "src/*/.spec.js" ] } [/javascript] Streuen Sie gegebenenfalls Optionen ein. Zweitens, der Test-Runner: [javascript] // jasmine.js import Jasmine from 'jasmine'; const jasmine = new Jasmine(); jasmine.loadConfigFile('jasmine.json') // Fügen Sie hier Ihren Lieblingsreporter ein // Ein onComplete-Handler ist erforderlich, um zu verhindern, dass Pre-Push-Hooks und dergleichen fehlgeschlagene Tests auslösen. jasmine.onComplete(passed => process.exit(passed ? 0 : 1)); jasmine.execute(); [/javascript] Fügen Sie schließlich der Einfachheit halber einige Aufgaben zu package.json hinzu: [javascript] { "scripts": { "test": "babel-node tools/run-jasmine.js", "watch": "babel-watch tools/run-jasmine.js --watch" [/javascript] } } Das Ausführen von npm run test sollte nun den Test-Runner starten. Was die Tests selbst betrifft, die umgeschrieben werden müssen, um keine Spuren von Angular zu hinterlassen, hier ein einfaches Beispiel: [javascript] import FooController from './foo.controller' let fooService, fooController describe('Der FooController', () => { beforeEach(() => { fooService = jasmine.createMock('fooService', ['bar']) fooController = new FooController(fooService) }) it('sollte etwas tun', () => { fooController.doTheThing(); expect(fooService.bar).toHaveBeenCalledWith('Ich werde die Sache tun') [/javascript] }) }) Dies läuft sehr schnell, da es keinen Overhead gibt und nur eine minimale Menge an Code geladen wird, und es gibt keine Spur von Angular oder anderen browserspezifischen Elementen. Profitieren Sie! Dieser Ansatz sollte für die meisten wichtigen Teile Ihrer Anwendung funktionieren: Controller und Dienste. Abhängigkeiten müssen ausgespielt oder durch ihre nativeren Versionen ersetzt werden. In unseren Tests laden wir zum Beispiel die Nodejs-Version von q als direkten Ersatz für $q. Eine Alternative wäre, $q durch einen vollständigen Mock-Service zu ersetzen, aber es ist ziemlich schwierig, die tatsächliche Mechanik von Promise zu reproduzieren.
Testen von asynchronem Code
In diesem Zusammenhang müssen wir einen anderen Mechanismus ersetzen, der in Angular beim Umgang mit asynchronem Code und Versprechen sehr häufig verwendet wird: $scope.$apply(). In den im Browser ausgeführten Tests löst dies eine Digest-Schleife und einen JS-Zyklus aus, so dass Callbacks ausgeführt werden können, bevor Sie Ihre Assertions ausführen.
Testen von DOM-manipulierendem Code
An dieser Stelle wird es knifflig und vage, denn ich bin noch nicht wirklich zu diesem Teil gekommen, aber mein Kollege hat ein wenig damit experimentiert und es geschafft, jsdom zum Ausführen von Tests zu verwenden, bei denen auch Angular zum Einsatz kam. Das verfehlt zwar teilweise den Zweck, aber es ermöglicht Ihnen, Spezifikationen zu erstellen und auszuführen, die DOM-Manipulationen, vor allem Direktiven, Komponenten und/oder Ansichten außerhalb des Kontexts eines Browsers testen. Natürlich ist es immer möglich, mehrere Testläufe in Ihrer Anwendung zu haben, getrennt nach DOM-Manipulationstests und logischen Tests.
Fazit
Dieser Test-Runner sollte es Ihnen ermöglichen, Ihre Tests - oder vor allem Ihren Test-Runner - so umzuschreiben, dass er viel schneller und mit viel weniger Speicher- und CPU-Overhead läuft. Außerdem werden Ihre Tests einfacher, es gibt weniger Boilerplate und weniger mentalen Aufwand, da Sie sich nicht mehr mit dem Laden der richtigen Angular-Module, der Registrierung von Mocks im DI-System von Angular und der Injektion des zu testenden Dienstes in die Anwendung beschäftigen müssen. Wenn Sie unter langen Testläufen leiden, sollten Sie diesen Ansatz in Erwägung ziehen. In unserem aktuellen Projekt haben wir (nur) etwa hundert Testfälle laufen (im Vergleich zu tausend für den traditionellen Test Runner), aber sie laufen auf meinem Rechner in etwa 0,06 Sekunden, während die anderen Tests mehrere Sekunden benötigen. Der größte Mehraufwand bei der Ausführung von Tests ist das Hochfahren von node-babel, das auf In-Memory-Puffer und dergleichen angewiesen ist, bevor es die maximale Geschwindigkeit erreicht. Für die Ausführung von Tests im 'Watch'-Modus sollte das kein Problem sein. Es könnte möglich sein, Babel im Daemon-Modus laufen zu lassen, um diesen (kurzen) Overhead zu reduzieren. Schließlich könnte dieser Ansatz auch für Angular 2 funktionieren; es hängt davon ab, ob Typescript und Angular 2 es Ihnen erlauben, Ihre Logik in einer Datei zu schreiben und Angular- oder Framework-spezifische Boilerplate in einer anderen Datei hinzuzufügen. Bei Angular 1 ist das der Fall, aber Angular 2 setzt auf Annotationen, was zu Problemen führen könnte. Natürlich sollte es möglich sein, die Annotationen bei der Ausführung von Tests einfach zu ignorieren. dieser Beitrag wurde auf / von meiner persönlichen Website gepostet.
Verfasst von

Freek Wielstra
Freek is an allround developer whose major focus has been on Javascript and large front-end applications for the past couple of years, building web and mobile web applications in Backbone and AngularJS.
Contact



