Wie teste ich ein System, bei dem die Objekte schwer zu verspotten sind?

34

Ich arbeite mit folgendem System:

Network Data Feed -> Third Party Nio Library -> My Objects via adapter pattern

Wir hatten kürzlich ein Problem, bei dem ich die von mir verwendete Version der Bibliothek aktualisiert habe. Dies führte unter anderem dazu, dass Zeitstempel (die von der Bibliothek eines Drittanbieters zurückgegeben werden long) von Millisekunden nach der Epoche auf Nanosekunden nach der Epoche geändert wurden.

Das Problem:

Wenn ich Tests schreibe, die die Objekte der Drittanbieterbibliothek verspotten, ist mein Test falsch, wenn ich bei den Objekten der Drittanbieterbibliothek einen Fehler gemacht habe. Zum Beispiel habe ich nicht bemerkt, dass sich die Genauigkeit der Zeitstempel geändert hat, was dazu führte, dass der Komponententest geändert werden musste, da mein Modell die falschen Daten zurückgab. Dies ist kein Fehler in der Bibliothek , es ist passiert, weil ich etwas in der Dokumentation verpasst habe.

Das Problem ist, ich kann nicht sicher sein, welche Daten in diesen Datenstrukturen enthalten sind, da ich ohne einen echten Datenfeed keine echten generieren kann. Diese Objekte sind groß und kompliziert und enthalten viele verschiedene Daten. Die Dokumentation für die Drittanbieter-Bibliothek ist schlecht.

Die Frage:

Wie kann ich meine Tests einrichten, um dieses Verhalten zu testen? Ich bin nicht sicher, ob ich dieses Problem in einem Komponententest lösen kann, da der Test selbst leicht falsch sein kann. Darüber hinaus ist das integrierte System groß und kompliziert und es ist leicht, etwas zu übersehen. In der obigen Situation hatte ich beispielsweise das Timestamp-Handling an mehreren Stellen korrekt angepasst, aber einen davon habe ich verpasst. Das System schien in meinem Integrationstest hauptsächlich die richtigen Dinge zu tun, aber als ich es für die Produktion bereitstellte (mit viel mehr Daten), wurde das Problem offensichtlich.

Ich habe momentan keinen Prozess für meine Integrationstests. Testen ist im Wesentlichen: Versuchen Sie, die Einheitentests in Ordnung zu halten, fügen Sie weitere Tests hinzu, wenn Probleme auftreten, und stellen Sie sie dann auf meinem Testserver bereit. Stellen Sie sicher, dass die Dinge vernünftig erscheinen. Dieses Problem mit dem Zeitstempel bestand die Komponententests, da die Mocks falsch erstellt wurden, und bestand dann den Integrationstest, weil keine unmittelbaren, offensichtlichen Probleme aufgetreten sind. Ich habe keine QS-Abteilung.

durron597
quelle
3
Können Sie einen echten Daten-Feed "aufnehmen" und später in der Bibliothek eines Drittanbieters "abspielen"?
Idan Arye
2
Jemand könnte ein Buch über solche Probleme schreiben. Tatsächlich hat Michael Feathers genau dieses Buch geschrieben: c2.com/cgi/wiki?WorkingEffectivelyWithLegacyCode In diesem Buch beschreibt er eine Reihe von Techniken, mit denen sich schwierige Abhängigkeiten auflösen lassen, damit Code besser getestet werden kann.
Cbojar
2
Der Adapter um die Fremdbibliothek? Ja, genau das empfehle ich. Diese Komponententests werden Ihren Code nicht verbessern. Sie werden es nicht zuverlässiger oder wartbarer machen. An diesem Punkt duplizieren Sie nur teilweise den Code eines anderen Benutzers. In diesem Fall duplizieren Sie einen schlecht geschriebenen Code aus dem Sound. Das ist ein Nettoverlust. In einigen Antworten wird empfohlen, einige Integrationstests durchzuführen. Das ist eine gute Idee, wenn Sie nur ein "Funktioniert das?" Gesundheitsüberprüfung. Gute Tests sind schwierig und erfordern ebenso viel Geschick und Intuition wie guten Code.
jpmc26
4
Ein perfektes Beispiel für das Böse der Einbauten. Warum die Bibliothek keine TimestampRückklasse (mit einem beliebigen Darstellung sie wollen) und liefert genannte Verfahren ( .seconds(), .milliseconds(), .microseconds(), .nanoseconds()) und natürlich den Namen Konstrukteure. Dann hätte es keine Probleme gegeben.
Matthieu M.
2
Der Spruch "Alle Probleme in der Codierung können durch eine Indirektionsebene gelöst werden (außer natürlich das Problem zu vieler Indirektionsebenen)" fällt hier ein.
Dan Pantry

Antworten:

27

Es hört sich so an, als ob Sie bereits Due Diligence betreiben. Aber ...

Nehmen Sie auf der praktischsten Ebene immer eine gute Handvoll der beiden Integrationstests für die vollständige Schleife für Ihren eigenen Code in Ihre Suite auf und schreiben Sie mehr Behauptungen, als Sie für nötig halten. Insbesondere sollten Sie über eine Handvoll Tests verfügen, die einen vollständigen Zyklus zum Erstellen, Lesen und Validieren ausführen.

[TestMethod]
public void MyFormatter_FormatsTimesCorrectly() {

  // this test isn't necessarily about the stream or the external interpreter.
  // but ... we depend on them working how we think they work:
  var stream = new StreamThingy();
  var interpreter = new InterpreterThingy(stream);
  stream.Write("id-123, some description, 12345");

  // this is what you're actually testing. but, it'll also hiccup
  // if your 3rd party dependencies introduce a breaking change.
  var formatter = new MyFormatter(interpreter);
  var line = formatter.getLine();
  Assert.equal(
    "some description took 123.45 seconds to complete (id-123)", line
  );
}

Und es hört sich so an, als würdest du schon so etwas machen. Sie haben es nur mit einer schuppigen und / oder komplizierten Bibliothek zu tun. In diesem Fall empfiehlt es sich, einige Testarten "So funktioniert die Bibliothek" einzufügen, die sowohl Ihr Verständnis der Bibliothek bestätigen als auch als Beispiele für die Verwendung der Bibliothek dienen.

Angenommen, Sie müssen verstehen und davon abhängig sein, wie ein JSON-Parser jeden "Typ" in einer JSON-Zeichenfolge interpretiert. Es ist hilfreich und trivial, so etwas in Ihre Suite aufzunehmen:

[TestMethod]
public void JSONParser_InterpretsTypesAsExpected() {
  String datastream = "{nbr:11,str:"22",nll:null,udf:undefined}";
  var o = (new JSONParser()).parse(datastream);

  Assert.equal(11, o.nbr);
  Assert.equal(Int32.getType(), o.nbr.getType());
  Assert.equal("22", o.str);
  Assert.equal(null, o.nll);
  Assert.equal(Object.getType(), o.nll.getType());
  Assert.isFalse(o.KeyExists(udf));
}

Denken Sie aber auch daran, dass automatisierte Tests jeglicher Art und bei nahezu jeder Genauigkeit Sie immer noch nicht vor allen Fehlern schützen können. Es ist durchaus üblich, Tests hinzuzufügen, wenn Sie Probleme entdecken. Da es keine QA-Abteilung gibt, werden viele dieser Probleme von den Endbenutzern entdeckt.

Und zu einem erheblichen Teil ist das ganz normal.

Und drittens, wenn eine Bibliothek die Bedeutung eines Rückgabewerts oder Feldes ändert, ohne das Feld oder die Methode umzubenennen oder auf andere Weise abhängigen Code zu "brechen" (möglicherweise durch Ändern des Typs), wäre ich mit diesem Herausgeber verdammt unzufrieden. Und ich würde argumentieren, dass Sie, obwohl Sie wahrscheinlich das Changelog hätten lesen sollen, wenn es eines gibt, wahrscheinlich auch einen Teil Ihres Stresses an den Verlag weitergeben sollten. Ich würde behaupten, sie brauchen die hoffentlich konstruktive Kritik ...

Svidgen
quelle
Ich wünschte, es wäre so einfach, einen Json-String in die Bibliothek einzuspeisen. Es ist nicht. Ich kann nicht das Äquivalent dazu tun (new JSONParser()).parse(datastream), da sie die Daten direkt von a NetworkInterfaceabrufen und alle Klassen, die das eigentliche Parsen durchführen, paketprivat und geschützt sind.
Durron597
Außerdem enthielt das Änderungsprotokoll nicht die Tatsache, dass die Zeitstempel von ms auf ns geändert wurden, und auch nicht die anderen Kopfschmerzen, die sie dokumentierten. Ja, ich bin sehr unglücklich mit ihnen, und ich habe es ihnen gegenüber zum Ausdruck gebracht.
Durron597
@durron597 Oh, das ist es fast nie. Sie können jedoch häufig die zugrunde liegende Datenquelle vortäuschen - wie im ersten Codebeispiel. ... Punkt ist: do Tests Integration Voll Schleife , wenn möglich, testen Sie Ihr Verständnis der Bibliothek , wenn möglich, und nur darüber im Klaren sein , dass Sie werden immer noch Fehler in die Wildnis lassen. Und Ihre Drittanbieter müssen dafür verantwortlich sein, unsichtbare, wichtige Änderungen vorzunehmen.
Svidgen
@ durron597 Ich bin nicht vertraut mit NetworkInterface... ist es etwas, in das Sie Daten einspeisen können, indem Sie die Schnittstelle mit einem Port auf localhost verbinden oder so?
Svidgen
NetworkInterface. Es ist ein Low-Level-Objekt zum direkten Arbeiten mit einer Netzwerkkarte und zum Öffnen von Sockets usw.
durron597 06.10.15
11

Kurze Antwort: Es ist schwer. Sie haben wahrscheinlich das Gefühl, dass es keine guten Antworten gibt, und das liegt daran, dass es keine einfachen Antworten gibt.

Lange Antwort: Wie @ptyx sagt , benötigen Sie Systemtests und Integrationstests sowie Komponententests :

  • Unit-Tests sind schnell und einfach durchzuführen. Sie fangen Fehler in einzelnen Codeabschnitten ab und verwenden Mocks, um deren Ausführung zu ermöglichen. Sie können notwendigerweise keine Fehlanpassungen zwischen Codeteilen erkennen (wie Millisekunden gegenüber Nanosekunden).
  • Integrationstests und Systemtests sind langsam (ähm) und schwierig (ähm) auszuführen, führen jedoch zu mehr Fehlern.

Einige konkrete Vorschläge:

  • Es hat einen gewissen Vorteil, einfach einen Systemtest durchführen zu lassen, um so viel System wie möglich laufen zu lassen. Auch wenn es nicht viel von dem Verhalten validieren kann oder das Problem sehr gut lokalisieren kann. (Micheal Feathers erläutert dies ausführlicher in Effektiv mit Legacy-Code arbeiten .)
  • Investition in Testbarkeit hilft. Es gibt eine große Anzahl von Techniken , die Sie hier verwenden können: die kontinuierliche Integration, Skripte, VMs, Werkzeuge wiedergeben, Proxy oder Umleitung des Netzwerkverkehrs.
  • Einer der (zumindest für mich) Vorteile einer Investition in die Testbarkeit liegt möglicherweise auf der Hand: Wenn das Schreiben oder Ausführen von Tests mühsam, nervig oder umständlich ist, kann ich sie unter Druck einfach überspringen oder müde. Es ist wichtig, dass Sie Ihre Tests unter der Schwelle "Es ist so einfach, dass es keine Entschuldigung gibt, dies nicht zu tun" halten.
  • Perfekte Software ist nicht machbar. Wie alles andere ist der Aufwand für das Testen ein Kompromiss, und manchmal lohnt sich der Aufwand nicht. Einschränkungen (wie das Fehlen einer QS-Abteilung) liegen vor. Akzeptieren Sie, dass Fehler auftreten, sich erholen und lernen.

Ich habe Programmieren als Lernaktivität für ein Problem und einen Lösungsraum beschrieben. Es ist vielleicht nicht möglich, alles im Voraus perfekt zu machen, aber Sie können es später lernen. ("Ich habe die Zeitstempelbehandlung an mehreren Stellen behoben, aber eine verpasst. Kann ich meine Datentypen oder Klassen ändern, um die Zeitstempelbehandlung expliziter und schwerer zu gestalten, oder um sie zentraler zu gestalten, sodass ich nur eine Stelle ändern kann? Kann ich Änderungen vornehmen Meine Tests zur Überprüfung weiterer Aspekte der Zeitstempelverarbeitung Kann ich meine Testumgebung vereinfachen, um dies in Zukunft zu vereinfachen? Kann ich mir ein Tool vorstellen, das dies vereinfacht hätte, und wenn ja, kann ich ein solches Tool bei Google finden? " Etc.)

Josh Kelley
quelle
7

Ich habe die Version der Bibliothek aktualisiert… was… dazu führte, dass Zeitstempel (die von der Bibliothek eines Drittanbieters zurückgegeben wurden long) von Millisekunden nach der Epoche in Nanosekunden nach der Epoche geändert wurden.

Dies ist kein Fehler in der Bibliothek

Ich bin absolut anderer Meinung als Sie hier. Es ist ein Fehler in der Bibliothek , in der Tat ein ziemlich heimtückischer. Sie haben den semantischen Typ des Rückgabewerts geändert, aber nicht den programmatischen Typ des Rückgabewerts. Dies kann alle Arten von Chaos anrichten, insbesondere, wenn es sich um eine geringfügige Beule handelt, aber auch, wenn es sich um eine größere Beule handelt.

Angenommen, die Bibliothek hat stattdessen einen Typ zurückgegeben MillisecondsSinceEpoch, einen einfachen Wrapper, der ein enthält long. Wenn sie es in einen NanosecondsSinceEpochWert geändert hätten, wäre Ihr Code nicht kompiliert worden und hätte Sie offensichtlich auf die Stellen hingewiesen, an denen Sie Änderungen vornehmen müssen. Die Änderung konnte Ihr Programm nicht unbemerkt beschädigen.

Besser noch wäre ein TimeSinceEpochObjekt, das seine Schnittstelle anpassen könnte, da mehr Präzision hinzugefügt wurde, z. B. das Hinzufügen einer #toLongNanosecondsMethode neben der #toLongMillisecondsMethode, ohne dass der Code geändert werden muss.

Das nächste Problem ist, dass Sie keine zuverlässigen Integrationstests für die Bibliothek haben. Sie sollten diese schreiben. Besser wäre es, eine Schnittstelle um diese Bibliothek herum zu erstellen, um sie vom Rest Ihrer Anwendung abzukapseln. Mehrere andere Antworten sprechen dies an (und mehr tauchen auf, während ich tippe). Integrationstests sollten seltener ausgeführt werden als Unit-Tests. Deshalb hilft eine Pufferschicht. Teilen Sie Ihre Integrationstests in einen separaten Bereich ein (oder benennen Sie sie anders), damit Sie sie nach Bedarf ausführen können, jedoch nicht jedes Mal, wenn Sie Ihren Komponententest ausführen.

cbojar
quelle
2
@durron597 Ich würde immer noch behaupten, dass es ein Bug ist. Warum sollte man, abgesehen von der fehlenden Dokumentation, das erwartete Verhalten ändern? Warum nicht eine neue Methode, die die neue Präzision bietet und die alte Methode immer noch Millis liefert? Und warum sollte der Compiler nicht eine Möglichkeit bieten, Sie über eine Änderung des Rückgabetyps zu informieren? Es braucht nicht viel, um dies klarer zu machen, nicht nur in der Dokumentation, sondern auch im Code.
Cbojar
1
@gbjbaanb, "dass sie schlechte Release-Praktiken haben" scheint mir ein Fehler zu sein
Arturo Torres Sánchez
2
@gbjbaanb Eine Drittanbieter-Bibliothek [sollte] mit ihren Benutzern einen "Vertrag" abschließen. Das Brechen dieses Vertrages - ob dokumentiert oder nicht - kann / sollte als Fehler angesehen werden. Wie bereits erwähnt , fügen Sie dem Vertrag eine neue Funktion / Methode hinzu , wenn Sie etwas ändern müssen (siehe alle ...Ex()Methoden in der Win32API). Wenn dies nicht möglich ist, wäre es besser gewesen, den Vertrag durch Umbenennen der Funktion (oder ihres Rückgabetyps) zu "brechen", als das Verhalten zu ändern.
TripeHound
1
Dies ist ein Fehler in der Bibliothek. Die Verwendung von Nanosekunden in einem langen Zeitraum ist eine Herausforderung.
Joshua
1
@gbjbaanb Du sagst, es ist kein Fehler, da es das beabsichtigte Verhalten ist, auch wenn es unerwartet ist. In diesem Sinne ist es kein Implementierungsfehler , aber es ist ein Fehler, der auch derselbe ist. Dies kann als Designfehler oder Schnittstellenfehler bezeichnet werden . Die Fehler liegen in der Tatsache, dass es eine primitive Besessenheit mit Longs und keine expliziten Einheiten aufweist, seine Abstraktion undicht ist, da Details seiner internen Implementierung exportiert werden (die Daten werden als Long einer bestimmten Einheit gespeichert) und dass sie verletzt werden das Prinzip des geringsten Erstaunens bei einem subtilen Einheitenwechsel.
cbojar
5

Sie benötigen Integrations- und Systemtests.

Unit-Tests eignen sich hervorragend, um zu überprüfen, ob sich Ihr Code erwartungsgemäß verhält. Wie Sie feststellen, stellt dies weder Ihre Annahmen in Frage noch stellt es sicher, dass Ihre Erwartungen vernünftig sind.

Es sei denn, Ihr Produkt hat nur eine geringe Interaktion mit externen Systemen oder es interagiert mit Systemen, die so gut bekannt, stabil und dokumentiert sind, dass sie zuverlässig verspottet werden können (dies ist in der Praxis selten der Fall) - Unit-Tests reichen nicht aus.

Je höher Ihre Tests sind, desto mehr schützen sie Sie vor dem Unerwarteten. Dies ist mit Kosten verbunden (Bequemlichkeit, Geschwindigkeit, Sprödigkeit ...), sodass Komponententests die Grundlage Ihrer Tests bleiben sollten, Sie jedoch andere Ebenen benötigen, einschließlich - möglicherweise - ein winziges Stück menschlicher Tests, die einen großen Beitrag zum Fangen leisten dumme Dinge, über die niemand nachdachte.

ptyx
quelle
2

Am besten erstellen Sie einen minimalen Prototyp und verstehen, wie die Bibliothek genau funktioniert. Auf diese Weise erhalten Sie einige Kenntnisse über die Bibliothek mit einer schlechten Dokumentation. Ein Prototyp kann ein minimalistisches Programm sein, das diese Bibliothek verwendet und die Funktionalität übernimmt.

Andernfalls macht es keinen Sinn, Komponententests mit halb definierten Anforderungen und einem schwachen Verständnis des Systems zu schreiben.

Was Ihr spezielles Problem betrifft - über die Verwendung falscher Metriken: Ich würde es als eine Änderung der Anforderungen behandeln. Wenn Sie das Problem erkannt haben, ändern Sie die Komponententests und den Code.

BЈовић
quelle
1

Wenn Sie eine beliebte, stabile Bibliothek verwenden, können Sie vielleicht davon ausgehen, dass sie Ihnen keine bösen Streiche spielt. Aber wenn Dinge wie das, was Sie beschrieben haben, mit dieser Bibliothek passieren, dann ist dies offensichtlich keine. Nach dieser schlechten Erfahrung müssen Sie jedes Mal, wenn bei Ihrer Interaktion mit dieser Bibliothek etwas schief geht, nicht nur die Möglichkeit untersuchen, dass Sie einen Fehler gemacht haben, sondern auch die Möglichkeit, dass die Bibliothek einen Fehler gemacht hat. Angenommen, dies ist eine Bibliothek, bei der Sie sich "unsicher" sind.

Eine der Techniken, die bei Bibliotheken angewendet werden, bei denen wir uns "unsicher" sind, besteht darin, eine Zwischenschicht zwischen unserem System und diesen Bibliotheken aufzubauen, die die von den Bibliotheken angebotenen Funktionen abstrahiert, die unsere Erwartungen an die Bibliothek für richtig hält und die auch erheblich vereinfacht Für unser zukünftiges Leben sollten wir beschließen, dieser Bibliothek den Start zu geben und sie durch eine andere Bibliothek zu ersetzen, die sich besser verhält.

Mike Nakis
quelle
Dies beantwortet die Frage nicht wirklich. Ich habe bereits einen Layer, der die Bibliothek von meinem System trennt, aber das Problem ist, dass mein Abstraktionslayer "Bugs" aufweisen kann, wenn sich die Bibliothek ohne Vorwarnung auf mich ändert.
Durron597
1
@ durron597 Dann isoliert der Layer die Bibliothek möglicherweise nicht ausreichend vom Rest Ihrer Anwendung. Wenn Sie feststellen, dass Sie diese Ebene nur schwer testen können, müssen Sie möglicherweise das Verhalten vereinfachen und die zugrunde liegenden Daten stärker isolieren.
Cbojar
Was @cbojar gesagt hat. Lassen Sie mich auch etwas wiederholen, das im obigen Text möglicherweise unbemerkt geblieben ist: Das assertSchlüsselwort (oder die Funktion oder Einrichtung, je nachdem, welche Sprache Sie verwenden) ist Ihr Freund. Ich spreche nicht von Behauptungen in den Unit- / Integrationstests. Ich sage, dass die Isolationsschicht mit Behauptungen sehr schwer sein sollte, um alles zu behaupten, was über das Verhalten der Bibliothek behauptet werden kann.
Mike Nakis
Diese Zusicherungen werden nicht unbedingt in Produktionsläufen ausgeführt, sondern werden während des Testens ausgeführt. Dabei wird eine White-Box-Ansicht Ihres Isolationsebenen angezeigt, und Sie können (so weit wie möglich) sicherstellen, dass die Informationen, die Ihre Ebene von der Bibliothek empfängt ist Ton.
Mike Nakis