Mock, mock, mocking on heaven's door


#1

Bisher habe ich ja mocking nur sehr eingeschränkt verwendet. (D.h. praktisch gar nicht). Nun muss ich aber einige Tests schreiben, bei denen ein bißchen was gemockt werden muss. Oder soll.

Die naive Frage, die sich mir da stellt: Woher weiß man, was genau wie gemockt wird?

Der Grund für die Frage ist eigentlich einfach: Ich habe den Eindruck, dass falsches Mocking schlicht die Ergebnisse verfälscht, und die Tests damit effektiv unbrauchbar werden. Ich würde annehmen, dass ein Mock gnadenlos und knallhart eine verbindliche, präzise und nicht verhandelbare Spezifikation widerspiegelt. Spätestens, wenn das, was der Mock macht, angepasst wird, damit der Test grün wird, läuft doch (offensichtlich?!) etwas falsch, oder nicht?

(BTW: Das Umfeld ist eigentlich node.js/express, wo irgendwelche REST-Webservices weggemockt werden. Die Krämpfe, die da durch die Infrastruktur und die Sprache verursacht werden, sind nochmal ein Thema für sich. Hier geht es eher um’s Konzept. Dass ich da an einigen Stellen den Eindruck habe, dass gar keine Funktionalität getestet wird, sondern schlicht getestet wird, ob richtig gemockt wurde (!), ist dann nochmal ein ganz anderer Punkt…)


#2

+1 fuer den Titel :slight_smile:

Da muss man jetzt unterscheiden, ob du Objekte mocken moechtest, also direkte Abhaenigigkeiten deinr Klasse die zB. im Konstruktor gesetzt werden, fuer isolierte Unit Tests.

Oder eben ob du externe Abhaengigkeiten stuben/mocken/faken moechtest, wie zB. “externe” REST Service fuer Integrationstests, fuer letzteres kannst du dir mal WireMock ansehen.

Wenn du denselben Test gegen den echten REST Service und gegen den Mock REST Service laufen lassen kannst (zB. nur durch Konfigaenderungen wie Host & Port), sollte der Test in beiden Faellen dasselbe Egebnis liefern :wink:
Damit kann man zB. sicherstellen dass das Mock und der echte Service sich gleich bzw. aehnlich verhalten.


#3

Ja, es geht um REST-mocking. Die Entscheidung, welche Mocking-Tools verwendet werden, wird anscheinend bei jeder API, die entwickelt werden muss, auf’s neue getroffen. Zuletzt jetzt https://github.com/node-nock/nock , aber auch https://mochajs.org/, und IIRC hab’ ich WireMock in einem anderen Teil auch schon gesehen.

Die entscheidende Frage, die ich jetzt mal suggestiv stelle: Ist es nicht so, dass man (egal, mit welchem Framework) einen Mock (entsprechend der Spec!) erstellt, und dann zuzusehen hat, dass die verwendenDe API damit klarkommt? Sonst besteht ja zwischen dem echten Code und den Tests ggf. gar kein Zusammenhang mehr…


#4

Gemockt wird, was eigentlich nicht getestet werden soll, aber für die Durchführung des Tests notwendig ist, und nicht so einfach “echt” verwendet werden kann. Typisch ist z.B. I/O-Komponenten zu mocken. Wenn du z.B. den Registrierungsprozess auf der Webseite testen willst, willst du die Registrierung-Mail nicht senden, aber den Inhalt abfangen, also mockst du deinen eMail-Service.

Wenn du zu viele Mocks brauchst, ist das natürlich ein Problem mit dem Design (z.B. Verwendung von Singletons). Im obigen Beispiel würde man z.B. besser ein Interface für den eMail-Service schreiben, so dass man für Tests einfach eine Dummy-Implementierung (statt des Mocks) verwenden könnte. Daraus folgt, dass idealerweise nur Komponenten gemockt werden sollten, die aus Bibliotheken u.s.w. kommen. In der Praxis kann man natürlich begründete Ausnahmen machen.


#5

Die Registrierung ist ein gutes Beispiel. (Seeehr gut…)

Angenommen, man hat irgendeinen Endpoint, der vom Browser aus angesprochen wird:

POST /user/register { name : 'foo', password: 'bar' }

Das ganze würde dann praktisch 1:1 an einen Backend-Service weitergeschleift, der die eigentliche Arbeit erledigt. Den könnte (?!) man dann mocken. Die Clientseitige Implementierung des Endpoints wäre im Pseudocode:

export default (req, res) => {
    const response = await post(backend + "/user/register", req.body);
    if (response.status == 200) return 200;
    return 500;
}

(dass so etwas in der Praxis dann nicht 3 Zeilen hat, sondern ca. 100, ist etwas, womit ich mich immer noch nicht abgefunden habe).

Der POST gegen das Backend soll nun also gemockt werden. Mit nock kann man dann sowas schreiben wie

nock(backend + "/user/register")
    .reply(200);

Aber eigentlich müßte ja die 200 dort haarklein an das angepasst werden, was der Spezifikation des Backends entspricht. Also eine 404 wenn irgendwas nicht gefunden wird, eine 500 wenn was im Argen liegt, eine 409 bei zu vielen Requests, eine X wenn Y …

(Das läuft doch dann darauf raus, dass man im Mock einen Großteil der Backend-Funktionalität nachimplementiert, strikt nach der Spezifikation…? (Nur die Zeile storeInDatabase(user) läßt man halt weg…) )

Oder nochmal allgemein: Wer stellt wann und wie sicher, dass sich der Mock genau so verhält, wie die echte Implementierung?


#6

So wie ich mocken verstanden habe (und es selber benutze) mockst du die Spezifikation. Also z.b. bei diesem und jenem Input kommt folgendes zurück oder es wird ein Fehler geworfen etc. Und das mockt man.

Ich mache es z.B. so dass ich die ganze Backendfunktionalität für die REST Interfaces mocke, ich gehe also davon aus dass dort alles funktioniert und teste nur ob die Handler entsprechend dem Verahlten des Backends das richtige machen.

Da wird dann meist nur die eine Methode gemockt die der Handler aufruft


#7

Du musst halt das entsprechende Verhalten der Schnittstelle (inkl. Fehlernachrichten und ähnliches) “nachimplementieren”. Deshalb sollte man Mocks auch nur dort einsetzen, wo es einen Mehrwert bringt. Der Vorteil eines Mocks in Deinem Beispiel ist natürlich, dass Du keinen laufenden Server benötigst, um den Test durchzuführen.

Wenn es aber einfacher ist, einen Server mit Testdaten bereit zu stellen (und das Registrieren keine ewigen Postprocessings wie das Versenden von Emails mit sich bringt), sollte man dies dann auch verwenden. Eventuell genügt auch für die Tests das Mocken des Email-Versendens.

Mocks sind etwas feines. Die Mocking-Hölle ist jedoch unbeherrschbar. :smiley:


#8

Dein Endpoint macht eigentlich zu wenig, um ihn zu testen. Er gibt ja lediglich ein 500er zurück. Das einzige hier wäre ob die Parameter korrekt weitergegeben werden an das backend.

So wie ich das Mocking kenne würde man verschiedene Szenarien überprüfen und für jedes einen eigenen mock verwenden.

Alles Ok
User existiert nicht
Password falsch
Backend liefert einen 304
Backend liefert einen 500
Backend nicht erreichbar
Backend überschreitet die Zeit.

Jedes dieser Szenarien bekommt einen mock, der das entsprechende Verhalten implementiert der dann getestet wird. Und dann wird geschaut wie dein Endpoint reagiert. Bei einem nicht vorhandenen User, wäre ein redirect zur Registrierung wohl angebracht.

Dein Endpoint könnte nun bei einem 500er auf ein alternatives Backend zugreifen. Der Mock für backend 1 liefert dann einen 500er und der Mock für backend 2 dann eben einen 200er.

Wenn man retries im Endpoint haben möchte, dann kann man im Mock überprüfen wie oft dieser aufgerufen wurde, und ob jedesmal die Parameter gestimmt haben.

Den Mock implementiert man auch nicht nach, sondern man hält ihn “dumm”. Man sagt einfach, was er zurückgeben soll. Egal was reinkommt und schaut dann evtl. noch ob das was reinkommt dem entspricht, was man erwartet was reinkommen soll.

Ein Vorteil von Mocks, kann hier sein, dass eine Testsuite mit vielen Tests, viele Requests abschickt die alle zwar nur wenige Sekunden brauchen, was sich aber insgesamt auf Minuten aufsummieren kann. Ein Mock der lokal läuft ist da in der Regel schneller und liefert die Ergebnisse in Sekunden.


#9

Die grundsätzliche Idee ist schon klar, und das ist auch sinnvoll. In der Java-Welt denke ich zwar, dass man im Idealfall nicht (oder selten) ein “echtes Mocking-Framework” braucht, das mit viel schwarzer Magie irgendwelche Methodenaufrufe intercepted und umleitet. Wenn (wie es IMHO sein sollte) die Klassen, die man eigentlich mocken würde, als interfaces vorliegen, kann man statt eines Mocks auch schlicht eine “Mock-Implementierung” verwenden (ja, vielleicht nicht immer, aber oft).

Aber gerade bei JavaScript/REST scheint mir da das Vehältnis zwischen Aufwand und Nutzen sehr ungünstig. Die Strategie…

… viele “kleine”, lokale Mocks zu erstellen ist das, worauf es jetzt rauszulaufen scheint. Das heißt vor jedem eigentlichen REST-Call steht ein

nock(url + "/foo").post("whavever").query(whatever).reply(123);

D.h. es gibt nicht “eine Instanz, die den Service komplett emuliert”, sondern kleine, lokale Instanzen, die NUR auf das reagieren, was für die jeweilige Anfrage zu erwarten ist. Aber das fühlt sich immernoch unbehaglich an.

nock(url + "/register").post().query(whatever).reply(200);  // Das 
callTestWithLongUserNameExpecting200();

nock(url + "/register").post().query(whatever).reply(200);  // ist 
callTestWithShortUserNameExpecting200();

nock(url + "/register").post().query(whatever).reply(200);  // so
callTestWithNormalUserNameExpecting200();

nock(url + "/register").post().query(whatever).reply(200);  // redundant
callTestWithUserNameThatContainsUmlautsExpecting200();

Natürlich kann man das in chai dann in die “setupBeforeEachTest”-Methode packen, aber wenn man die Tests dann entsprechend gruppieren muss, wird das ggf. auch unübersichtlich.

Außerdem besteht immer die Gefahr, dass man dann einen dieser “lokalen Mocks” genau so macht, wie es sein muss, damit der Test durchläuft (auch wenn der echte Service sich anders verhält). Spätestens, wenn man feststellt, dass man Widersprüche drin hat…

nock(url + "/").get().reply(200);
callTestExpecting200();

nock(url + "/").get().reply(202);
callTestExpecting202();

bringen einem die “grünen Tests” gerade mal gar nichts mehr. Irgendwie wirkt das ganze recht unsystematisch und wackelig. Aber vielleicht muss man sich mehr dran gewöhnen.

(Dass man nicht einen einzelnen Test durchlaufen lassen kann, und dass es kaum eine praktikable Möglichkeit gibt, mit einem Debugger Schritt für Schritt durch die Tests zu laufen, wäre eigentlich Teil eines meiner üblichen Rants über JavaScript, crappy tools, stümperhaft zusammengeschusterte Libs und verkorkste Infrastrukturen, aber … ich kann es einfach nicht unerwähnt lassen. Vollkommen anachronistisch…)


#10

Die Frage hier ist: wie wahrscheinlich ist es, dass sich die Methoden, die nicht im aktuellen Test benötigt werden, in Zukunft ändern?

Denn die Grundregel bei jeder Art Test ist, dass er nur dann fehlschlagen soll (Wobei “compiliert nicht” auch zählt), wenn die getestete Funktionalität verändert wurde, nicht weil sich etwas in der Umgebung (wie eben eine nicht verwendete Methode in einem Interface) geändert ist.

Mocking-Frameworks (wie Mockito) kapseln mich da ab. Eigene Mock-Implementierungen muss ich bei jeder Interface-Änderung nachziehen.

bye
TT


#11

Nun, der letzte Punkt wird IMHO dadurch weitgehend obsolet, dass in einem (Java) Interface ja “wenige” Methoden stehen sollTen, und es eine Spec repräsentieren sollTe, die sich “nie” ändert.

Bei REST ist das ja etwas anders. Es macht keinen Sinn, zu versuchen, den REST-Service 1:1 als Mock nachzuimplementieren und nur die “writeToDatabase”-Zeile wegzulassen - gerade bei komplexeren Services. Aber viele der Fragen stehen immernoch im Raum, wenn man vor jedem Test-Call NUR den REST-Path mockt, von dem man erwartet, dass er aufgerufen wird.

(Die (unrealistische) “Idealvorstellung” wäre ja, dass man die (Unit- oder Integration) Tests sowohl über den Mock als auch die echte Anwendung laufen lassen kann - aber das ist wohl nur Träumerei…)


#12

Aber die “writeToDatabase”-Zeile in eine eigene Klasse zu extrahieren, die der komplexe Service dann injiziert bekommt macht extrem viel Sinn…

bye
TT


#13

Wenn man diese eine Funktionalität so rausziehen kann, würde das sicher Sinn machen. In Java würde man da natürlich ein Interface erstellen.

(In JavaScript scheint (?!) es ja üblich zu sein, Code etwas … “pragmatischer hinzuschreiben” *räusper*. Wobei das auch schlicht auf fehlende “best practices” im konkreten Fall zurückzuführen sein kann).

Dann würde sich schon wieder die Frage stellen, wie viele “Schichten” denn mit diesem Test getestet werden sollen. (Dass ich mir gerade nicht sicher bin, ob das ganze nun Unit- oder Integrationstests sein sollen, sagt vermutlich schon viel (nicht positives) aus, aber den üblichen Programmierer-Weltschmerz hier auszubreiten, würde wohl nicht viel Sinn machen).

Jetzt läuft es wohl darauf raus, dass vor jedem Test genau der Mock erstellt wird, der für den Test erwartet wird. Das macht zumindest die Struktur der Tests einigermaßen übersichtlich. Ob der Mock sich so verhält, wie der echte Service, wird sich zeigen. (Ich habe da so eine Vermutung, aber … ich werde zumindest versuchen, auf irgendwas in Richtung einer Swagger-Schnittstellen-Spec hinzuarbeiten, vielleicht hilft das schon…)


#14

Wo du’s gerade sagst, eigentlich ein Dreizeiler: https://github.com/EDSM-NET/EDDN/blob/master/examples/NodeJS/index.js , wird zu: https://github.com/EDSM-NET/EDDN/blob/master/examples/Java/src/main/java/org/eddn/examples/EddnPump.java . :roll_eyes: Wobei ich das mit dem poller auch noch nicht ganz… hab. :thinking: