Tipps und Tricks für AngularJS, Teil 6: Backend-lose Entwicklung mit ngMockE2E

Damit Frontend- und Backend-Entwickler parallel arbeiten können, sind Hilfsmittel wie Mock-Objekte nötig, um Funktionen des Backends zu simulieren. AngularJS bietet zu dem Zweck ngMockE2E an.

In Pocket speichern vorlesen Druckansicht 6 Kommentare lesen
Lesezeit: 12 Min.
Von
  • Vildan Softic
Inhaltsverzeichnis

Damit Frontend- und Backend-Entwickler parallel arbeiten können, sind Hilfsmittel wie Mock-Objekte nötig, um Funktionen des Backends zu simulieren. AngularJS bietet zu dem Zweck ngMockE2E an.

Ein großer Vorteil von REST ist die einheitliche Kommunikation, durch welche die Art der Umsetzung der Backend-API für Frontend-Entwickler belanglos ist. Bei klar getrennten Teams, die sich die Arbeit für Front- und Backend aufteilen, kommt es jedoch spätestens beim Thema Backend-Kommunikation zu Überschneidungen. Wie in der Abbildung zu sehen, ist die parallele Arbeit zwar theoretisch möglich, in der Praxis zeichnet sie sich jedoch oftmals durch längere Wartezeiten aus.

Um derlei Probleme zu umgehen, ist es ratsam, vor Beginn der Entwicklung Endpunkte zu definieren. Sie bieten verbindliche Schnittstellen für beide Seiten. Frontendentwickler können sie nach dem Festlegen durch Platzhalter (auch Attrappen oder Mock-Objekte genannt) darstellen und zu einem späteren Zeitpunkt gegen echte Endpunkte austauschen.

Um Funktionen des Frontends überprüfen zu können, ist häufig eine Schnittstelle zum Backend nötig, auf die man im Zweifel warten muss.

Der Artikel demonstriert das Vorgehen anhand eines Ticket-Systems, das die in Tabelle 1 gezeigten Endpunkte verwendet. Dabei ist hervorzuheben, dass sowohl für den Update- als auch für den Create-Vorgang die Methode POST zum Einsatz kommt und auf PUT verzichtet wird. Das Beispiel samt Code ist online auf Plunker zu finden.

Methode Route Anfragedaten Beschreibung
GET /ticket Keine Anfordern aller Tickets
GET /ticket Keine Anfordern eines durch den Parameter :id gekennzeichneten Tickets
POST /ticket Ticket-Entität Erstellen eines neuen oder aktualisieren eines Tickets
DELETE /ticket Keine Löschen eines Tickets mittels Parameter :id

Tabelle 1: Endpunkte für das Ticket-System

Damit Frontend-Entwickler während der Programmierung des Backends nicht warten müssen, stellt AngularJS das Modul ngMockE2E bereit, mit dem sich Mock-Objekte zum Umgang mit ausgehenden Anfragen hinterlegen lassen. Im Rahmen von Akzeptanz- beziehungsweise E2E-Tests ermöglichen sie es, einzelne Anfragen von AngularJS abzufangen und mit einer vorgefertigten Nachricht zu antworten.

Neben dem Testen ist die Backend-lose Entwicklung eine Einsatzmöglichkeit des Moduls, die für das zuvor beschriebene Szenario getrennter Teams geeignet ist. Der Frontend-Entwickler erstellt in dem Fall eine Art interaktiven Platzhalter für die tatsächliche Schnittstelle, der dynamische Antworten liefert und somit die korrekte Implementierung von HTTP-Anfragen in der Anwendung ermöglicht. Steht zu einem späteren Zeitpunkt die echte API zur Verfügung, muss der Entwickler lediglich den Platzhalter deaktivieren.

Um ngMockE2E zu verwenden, ist es notwendig, das externe Modul per Skriptreferenz einzubinden:

[...]
<script src="./js/angular.min.js"></script>
<script src="./js/angular-mocks.js"></script>
[...]

Der nächste Code-Ausschnitt zeigt, wie der REST-Platzhalter für das Ticket-System-Beispiel aufgesetzt ist.

var app = angular.module('ngResourceDemo', ['ngResource', 'ngMockE2E']);
app.run(function($httpBackend) {
// (1) Beispielhafte Ticket-Entität
var ticket = function(
id, title, assignedTo, description, percentageComplete) {
this.id = id;
this.title = title;
this.assignedTo = assignedTo;
this.description = description;
this.percentageComplete = percentageComplete;
};

// (2) Exemplarische Datenbank mit Sammlung von Tickets
var tickets = [
new ticket(
'1', 'Fehlende Exception', 'Max Mustermann',
'Die Methode XYZ liefert keine Exception im
Fehlerfall ABC', 0),
new ticket(
'2', 'Ticket-Service einbinden', 'Max Mustermann',
'Fehler sollen im Ticketing-Service protokolliert werden', 24),
new ticket(
'3', 'AngularJS updaten', 'Max Mustermann',
'Es soll auf die aktuelle Version 1.3 upgedated werden', 0),
new ticket(
'4', 'Rahmen fehlt', 'Susi Sorglos',
'Das div.demo hat keinen Border definiert', 100),
new ticket(
'5', 'Responsive Grid einführen', 'Susi Sorglos',
'Die Applikation sollte ein Responsive Grid
verwenden', 17),
];
// (3) Regex-Ausdruck um das Vorhandensein
// des ID Parameters zu prüfen
var regexGetTicket = new RegExp('/ticket/([0-9]+)');

// (4) alle Tickets retournieren
// GET: /ticket
$httpBackend.whenGET('/ticket').respond(tickets);


// (5) ein Ticket mittels übereinstimmender ID retournieren
// GET: /ticket/:id
$httpBackend.whenGET({
test: function(url) {
return regexGetTicket.test(url);
}
}).respond(function(method, url) {
var id = url.match(regexGetTicket)[1];
var match = _.find(tickets, function(t) {
return t.id === id;
});

// (6) Die Antwort ist ein HTTP-Response-Status
// mit einer Payload.
// Abhängig davon ob das Objekt gefunden wurde,
// liefert die Methode den passenden Status und Wert zurück
return match === undefined ? [404, 'Not found'] : [200, match];
});


// (7) Hinzufügen oder Überschreiben eines Tickets
// POST: /ticket
$httpBackend.whenPOST('/ticket').respond(function(method,
url, data) {
var newTicket = angular.fromJson(data);
var existingTicket = _.find(tickets, function(t) {
return t.id === newTicket.id;
});

if(existingTicket) {
_.extend(existingTicket, newTicket);
return [200, existingTicket, {}];
} else {
newTicket.id = parseInt(_.max(tickets, function(t) {
return t.id;
}).id || 0, 10) + 1 + '';

tickets.push(newTicket);
return [200, newTicket];
}
});

// (8) Löschen eines Tickets
// DELETE: /ticket/:id
$httpBackend.whenDELETE({
test: function(url) {
return regexGetTicket.test(url);
}
}).respond(function(method, url) {
var id = url.match(regexGetTicket)[1];
var match = _.find(tickets, function(t) {
return t.id === id;
});

var pos = _.indexOf(_.pluck(tickets, 'id'), id);
tickets.splice(pos, 1);

return match === undefined ? [404, 'Not found'] : [200, match];
});
// (9) Hinweis auf Attrappe
console.log('Backend-Attrappe aktiv!');
});