Es sollte allgemein bekannt sein, dass Sie sich bei bestimmten Arten von automatisierten Tests aus einer Reihe von Gründen nicht auf die Verfügbarkeit externer Dienste verlassen wollen:
- Betriebszeit des besagten Dienstes (Ihre Tests schlagen fehl, wenn der Dienst nicht verfügbar ist)
- Dynamische Natur der Daten (macht Ihre Behauptungen schwieriger)
- Ausführungsgeschwindigkeit Ihrer Tests
- Übermäßige Belastung des Dienstes
- usw.
Im Idealfall verwerfen Sie daher den externen Dienst. In Ihren Unit-Tests tun Sie das zum Beispiel mit Mock-Objekten. Bei Integrationstests ist dies schwieriger - Sie verwenden keine Mock-Objekte in Integrationstests, da dies das beobachtete Verhalten Ihrer Anwendung verändern könnte.
In einem unserer Projekte kämpfen wir schon seit geraumer Zeit mit diesem Problem. Es besteht aus zwei Hauptkomponenten, einer iPhone-App und einer serverseitigen Komponente, die beide mit einem externen Webservice kommunizieren, um die Daten abzurufen, die in der App angezeigt und auf dem Server bearbeitet werden sollen. In unseren Integrationstests haben wir einfach den Produktions-Webservice verwendet und einige oberflächliche Assertions auf das Ergebnis angewandt, mit unterschiedlichen Ergebnissen.
- Liefern Sie feste, vorhersehbare Ergebnisse mit spezifischen, anerkannten Anfragen
- Leiten Sie die Anfrage an den aktuell verwendeten Live-Webservice weiter, damit unsere bestehenden Tests nicht alle abbrechen
- (später) Fügen Sie eine Funktion hinzu, um die zurückgegebenen Daten variabel zu machen, da einige Tests darauf angewiesen sind, dass die zurückgegebenen Testdaten in der Zukunft liegen.
- Kompromittieren Sie nicht die Sicherheit - der Live-Webservice erfordert eine HTTP-Authentifizierung.
Natürlich musste es auch schnell gehen. Wir haben die Erstellung dieses gefälschten Webservice eine Weile aufgeschoben, weil es uns als eine Menge Arbeit erschien, aber als wir uns schließlich dafür entschieden, dachten wir uns: "Wie schwer kann es schon sein?". Wir haben schon eine Weile auf eine Gelegenheit gewartet, NodeJS zu verwenden, und soweit wir sehen konnten, war dies in diesem Fall die ideale Wahl - wir haben einen REST-ähnlichen Webservice (readonly), der hauptsächlich i/o (vom Dateisystem und dem externen Webservice) durchführt, und er sollte einfach und leicht zu erstellen sein. Also machten wir uns in ein paar Schritten ans Hacken. Lesen Sie mehr für den ganzen Artikel und den Code.
1. Erstellen Sie einen Webserver, der auf Anfragen reagiert
Das ist in Node ganz einfach. Lassen Sie es auch einige Informationen für uns ausgeben und machen Sie seinen Port konfigurierbar, damit wir es dauerhaft auf einem Port laufen lassen können und es einen anderen Port verwenden kann, wenn es automatisch als Teil der Integrationstestaufgabe gestartet wird:
[sourcecode language="js"]
var server = http.createServer(function(request, response) {
var params = url.parse(request.url);
console.log("request params: " + util.inspect(params));
response.end("woot");
});
var port = process.argv[2] || 1337;
server.listen(port)
console.log("Server running at https://xebia.com/blog:" + port);[/sourcecode]
2. Bestimmte Anfragen abgleichen und ein erwartetes Ergebnis für diese zurückgeben
In der ersten Version wurde einfach ein Array von Objekten mit einem festen Pfad und einer Datei verwendet: Grundsätzlich:
[sourcecode language="js"]
var responseMap = [
{path:"/mobile-api-planner?fromStation=ASD&toStation=RTD",
file:"asd-rtd.xml"}
{path:"/mobile-api-planner?fromStation=LW&toStation=RTD",
file:"lw-rtd.xml"}
][/sourcecode]
Aber wie sich herausstellt, würde das nicht funktionieren, weil die Reihenfolge der Parameter einfach nicht konstant ist. Was ist also die nächste Option? Wir könnten mit einem Array von Parametern herumspielen, um sie alle abzugleichen, aber das wäre eine Menge Code und möglicherweise fehleranfällig, also haben wir stattdessen reguläre Ausdrücke verwendet. Ich habe reguläre Ausdrücke noch nie gemocht, ich habe sie einfach nicht verstanden. Aber nach einigem Googeln und Ausprobieren habe ich es schließlich geschafft, sie zum Laufen zu bringen. Also, eine Antwortkarte mit regulären Ausdrücken und einem Dateinamen und eine einfache Suchmethode, die einen passenden Dateinamen findet:
[sourcecode language="js"]var responseMap = [
// matches on mobile-api-planner w/ params hslAllowed=true, fromStation=ASD, toStation=RTD
{pattern:/bmobile-api-planner?(?=.*hslAllowed=true)(?=.*fromStation=ASD)(?=.*toStation=RTD)/,
response:{file:"ams-rtd-fyra.xml"}},
// matches on mobile-api-planner w/ params fromCity=Purmerend, fromStreet=Nijlstraat, toStation=RTD
{pattern:/bmobile-api-planner?(?=.*fromCity=Purmerend)(?=.*fromStreet=Nijlstraat)(?=.*toStation=RTD)/,
response:{file: "pmr-nijlstraat-rtd-fyra.xml"}},
{pattern:/bmobile-api-planner?(?=.*fromStation=LW)(?=.*toStation=UT)/,
response:{file: "lwd-ut-trip-cancelled.xml"}}
];
function findResponseFor(path) {
var result = null;
responseMap.forEach(function(candidate) {
if (path.match(candidate.pattern)) {
result = candidate.response;
return;
}
})
return result;
}
// file read function, simply writes a file with the given name
// from the 'response/' directory to the response.
function writeFileToResponse(responseFile, response) {
var filePath = path.join(__dirname, 'response', responseFile);
response.writeHead(200, {"Content-Type": "application/xml"});
// may want to consider using fileSystem.createReadStream etc instead
// for chunked responses and (probably) better memory usage
fileSystem.readFile(filePath, 'UTF-8', function(err, data) {
response.end(data);
});
}
// and the new request handler
var server = http.createServer(function(request, response) {
var params = url.parse(request.url);
var responseFile = findResponseFor(params.path);
if (responseFile != null) {
writeFileToResponse(responseFile, response);
} else {
console.log("Response not found");
}
});[/sourcecode]
3. Proxy-Anfragen an den echten Webservice, wenn keine Antwortdatei gefunden wurde
Es gibt bereits ein else für den Fall, dass es keine Übereinstimmung in den festen Antworten gibt, also lassen Sie uns das einfach erweitern:
[sourcecode language="js"]var responseFile = findResponseFor(params.path);
if (responseFile != null) {
writeFileToResponse(responseFile, response);
} else {
console.log("Response not found");
writeWebserviceToResponse(request, response);
} [/sourcecode]
Next, create a simple proxy using http.get and the parameters in the response. This could probably be done even simpler though. We also forward a HTTP authentication header if it exists. Coincidentially, if you call the proxy through a browser, it will still automatically ask you to provide HTTP authentication credentials - very convenient
[sourcecode language="js"]
function writeWebserviceToResponse(request, response) {
var params = url.parse(request.url);
var options = {
host: 'webservices.ns.nl',
path: params.path,
headers: {'Authorization' : request.headers.authorization}
};
var req = http.get(options, function(res) {
res.setEncoding('utf8');
response.writeHead(res.statusCode, res.headers);
res.on('data', function (chunk) {
response.write(chunk);
});
res.on('end', function() {
response.end();
});
});
req.on('error', function(e) {
console.log('problem with request: ' + e.message);
});
}[/sourcecode]
4. Machen Sie einen Teil des Ergebnisses variabel
Wie bereits erwähnt, benötigen wir unter anderem etwas variable Daten. In einigen Fällen müssen die festen Antworten Datumsangaben relativ zum aktuellen Datum enthalten, zum Beispiel eine halbe Stunde in der Zukunft. Wir benötigen dynamisch generierte Daten, die bei der Ausführung der Anfrage erzeugt werden, und platzieren sie in der zurückgegebenen Datei.
- Einrichten und Kompilieren der Vorlagen
- Dynamische Parameter in einigen Antworten erstellen
- Ersetzen Sie die Methode writeFileToResponse, um die Vorlagen mit den dynamischen Antworten zu rendern
[sourcecode language="js"]
// Mu initialization code:
var Mu = require('mu');
var responseDir = path.join(__dirname, 'response')
Mu.root = responseDir;
// pre-compile the templates.
// https://github.com/raycmorgan/Mu/issues/14
fileSystem.readdir(responseDir, function(err, files) {
files.forEach(function(file) {
console.log('Compiling template file ' + file)
Mu.compile(file, function(err, parsed) {
if (err) { throw err; }
});
});
});
// Dynamic response example:
var responseMap = [
{ pattern:/bmobile-api-planner?(?=.*fromStation=MDB)(?=.*toStation=UT)/,
response: {
file: "mdb-ut-trip-cancelled.xml",
params: {
departure: function() {
var now = new Date();
return formatDate(now.setMinutes(now.getMinutes() + 32));
}
}
}
}
];
// New writeFileToResponse method:
function writeFileToResponse(responseFile, response) {
response.writeHead(200, {"Content-Type": "application/xml"});
Mu.render(responseFile.file, responseFile.params)
.addListener('data', function(chunk) {
response.write(chunk)
})
.addListener('end', function() {
response.end()
});
}[/sourcecode]
Und da haben Sie es. Ein gefälschter Proxy-Webservice mit Authentifizierung und dynamischen Feldern, der an einem Freitagnachmittag in ein paar Stunden zusammengehackt wurde. Funktioniert bis jetzt einwandfrei. Weniger als 100 Zeilen ohne die Liste der Antwortabgleiche und Parameter, die wahrscheinlich am umfangreichsten sein werden. Ich habe den gesamten Code (mit gekürzten Matchern) auf Gist gestellt - viel Spaß.
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



