Ist es in Ordnung, einen Teil der getesteten Klasse vorzutäuschen?

22

Angenommen, ich habe eine Klasse (vergib das erfundene Beispiel und das schlechte Design davon):

class MyProfit
{
  public decimal GetNewYorkRevenue();
  public decimal GetNewYorkExpenses();
  public decimal GetNewYorkProfit();

  public decimal GetMiamiRevenue();
  public decimal GetMiamiExpenses();
  public decimal GetMiamiProfit();

  public bool BothCitiesProfitable();

}

(Beachten Sie, dass die Methoden GetxxxRevenue () und GetxxxExpenses () Abhängigkeiten aufweisen, die ausgeblendet sind.)

Jetzt teste ich BothCitiesProfitable (), was von GetNewYorkProfit () und GetMiamiProfit () abhängt. Ist es in Ordnung, GetNewYorkProfit () und GetMiamiProfit () zu stubgen?

Anscheinend teste ich GetNewYorkProfit () und GetMiamiProfit () zusammen mit BothCitiesProfitable () gleichzeitig, wenn dies nicht der Fall ist. Ich muss sicherstellen, dass ich das Stubbing für GetxxxRevenue () und GetxxxExpenses () einrichte, damit die GetxxxProfit () -Methoden die richtigen Werte zurückgeben.

Bisher habe ich nur Beispiele für Stichprobenabhängigkeiten von externen Klassen gesehen, nicht von internen Methoden.

Und wenn es in Ordnung ist, gibt es ein bestimmtes Muster, das ich verwenden sollte, um dies zu tun?

AKTUALISIEREN

Ich mache mir Sorgen, dass wir das Kernthema übersehen könnten, und das ist wahrscheinlich die Schuld meines schlechten Beispiels. Die grundlegende Frage lautet: Wenn eine Methode in einer Klasse von einer anderen öffentlich zugänglichen Methode in derselben Klasse abhängt, ist es in Ordnung (oder sogar empfohlen), diese andere Methode zu entfernen?

Vielleicht fehlt mir etwas, aber ich bin mir nicht sicher, ob es immer Sinn macht, die Klasse aufzuteilen. Vielleicht wäre ein anderes minimal besseres Beispiel:

class Person
{
 public string FirstName()
 public string LastName()
 public string FullName()
}

wobei vollständiger Name definiert ist als:

public string FullName()
{
  return FirstName() + " " + LastName();
}

Ist es in Ordnung, beim Testen von FullName () FirstName () und LastName () zu stubben?

Benutzer
quelle
Wenn Sie einen Code gleichzeitig testen, wie kann das schlecht sein? Was ist das Problem? Normalerweise sollte es besser sein, den Code öfter und intensiver zu testen.
Benutzer unbekannt
Ich habe gerade von Ihrem Update erfahren und meine Antwort aktualisiert
Winston Ewert

Antworten:

27

Sie sollten die betreffende Klasse aufteilen.

Jede Klasse sollte eine einfache Aufgabe erledigen. Wenn Ihre Aufgabe zum Testen zu kompliziert ist, ist die Aufgabe, die die Klasse ausführt, zu groß.

Ignoriere die Dummheit dieses Designs:

class NewYork
{
    decimal GetRevenue();
    decimal GetExpenses();
    decimal GetProfit();
}


class Miami
{
    decimal GetRevenue();
    decimal GetExpenses();
    decimal GetProfit();
}

class MyProfit
{
     MyProfit(NewYork new_york, Miami miami);
     boolean bothProfitable();
}

AKTUALISIEREN

Das Problem bei Stub-Methoden in einer Klasse besteht darin, dass Sie die Kapselung verletzen. Bei Ihrem Test sollte geprüft werden, ob das äußere Verhalten des Objekts mit den Spezifikationen übereinstimmt. Was auch immer im Inneren des Objekts passiert, geht ihn nichts an.

Die Tatsache, dass FullName FirstName und LastName verwendet, ist ein Implementierungsdetail. Nichts außerhalb der Klasse sollte sich darum kümmern, dass das wahr ist. Indem Sie die öffentlichen Methoden verspotten, um das Objekt zu testen, nehmen Sie an, dass das Objekt implementiert ist.

Irgendwann in der Zukunft könnte diese Annahme nicht mehr zutreffen. Möglicherweise wird die gesamte Namenslogik auf ein Namensobjekt verlagert, das die Person einfach aufruft. Möglicherweise greift FullName direkt auf die Mitgliedsvariablen first_name und last_name zu, anstatt FirstName und LastName aufzurufen.

Die zweite Frage ist, warum Sie das Bedürfnis haben, dies zu tun. Schließlich könnte Ihre Personenklasse wie folgt getestet werden:

Person person = new Person("John", "Doe");
Test.AssertEquals(person.FullName(), "John Doe");

Sie sollten nicht das Gefühl haben, etwas für dieses Beispiel zu brauchen. Wenn Sie dann tun, sind Sie stumm und gut… hören Sie es auf! Es hat keinen Vorteil, die Methoden dort zu verspotten, da Sie sowieso die Kontrolle darüber haben, was darin enthalten ist.

Der einzige Fall, in dem es sinnvoll erscheint, die von FullName verwendeten Methoden zu verspotten, ist, wenn es sich bei FirstName () und LastName () um nicht triviale Operationen handelt. Vielleicht schreiben Sie einen dieser zufälligen Namensgeneratoren, oder Vorname und Nachname fragen die Datenbank nach Antworten ab oder so. Wenn dies jedoch der Fall ist, deutet dies darauf hin, dass das Objekt etwas tut, das nicht zur Klasse Person gehört.

Anders ausgedrückt bedeutet das Verspotten der Methoden, das Objekt in zwei Teile zu zerlegen. Ein Teil wird verspottet, während das andere Teil getestet wird. Was Sie tun, ist im Wesentlichen ein Ad-hoc-Auflösen des Objekts. Wenn dies der Fall ist, brechen Sie das Objekt bereits auf.

Wenn Ihre Klasse einfach ist, sollten Sie nicht das Bedürfnis verspüren, Teile davon während eines Tests zu verhöhnen. Wenn Ihre Klasse so komplex ist, dass Sie sich lustig fühlen, sollten Sie sie in einfachere Teile aufteilen.

UPDATE WIEDER

So wie ich es sehe, hat ein Objekt äußeres und inneres Verhalten. Externes Verhalten umfasst Rückgabewerte, Aufrufe an andere Objekte usw. Natürlich sollte alles in dieser Kategorie getestet werden. (Was würdest du sonst testen?) Aber das interne Verhalten sollte eigentlich nicht getestet werden.

Jetzt wird das interne Verhalten getestet, denn es führt zum externen Verhalten. Aber ich schreibe keine Tests direkt über das interne Verhalten, nur indirekt über das externe Verhalten.

Wenn ich etwas testen möchte, sollte es verschoben werden, damit es zu einem externen Verhalten wird. Deshalb denke ich, wenn Sie etwas verspotten möchten, sollten Sie das Objekt so aufteilen, dass das, was Sie verspotten möchten, jetzt im äußeren Verhalten der fraglichen Objekte liegt.

Aber welchen Unterschied macht es? Wenn FirstName () und LastName () Mitglieder eines anderen Objekts sind, ändert sich dann wirklich das Problem von FullName ()? Wenn wir entscheiden, dass es notwendig ist, FirstName und LastName zu verspotten, hilft es dann tatsächlich, dass sie sich auf einem anderen Objekt befinden?

Ich denke, wenn Sie Ihren spöttischen Ansatz verwenden, dann erstellen Sie eine Naht im Objekt. Sie haben Funktionen wie Vorname () und Nachname (), die direkt mit einer externen Datenquelle kommunizieren. Sie haben auch FullName (), was nicht der Fall ist. Aber da sie alle in derselben Klasse sind, ist das nicht ersichtlich. Einige Teile sollen nicht direkt auf die Datenquelle zugreifen, andere nicht. Ihr Code wird klarer, wenn Sie nur diese beiden Gruppen aufteilen.

BEARBEITEN

Machen wir einen Schritt zurück und fragen: Warum verspotten wir Objekte, wenn wir testen?

  1. Führen Sie die Tests konsistent aus (vermeiden Sie den Zugriff auf Dinge, die sich von Lauf zu Lauf ändern).
  2. Vermeiden Sie den Zugriff auf teure Ressourcen (nicht auf Dienste von Drittanbietern usw.)
  3. Vereinfachen Sie das zu testende System
  4. Erleichtern Sie das Testen aller möglichen Szenarien (z. B. Simulation von Fehlern usw.).
  5. Vermeiden Sie es, von den Details anderer Codeteile abzuhängen, damit Änderungen an diesen anderen Codeteilen diesen Test nicht unterbrechen.

Ich denke, die Gründe 1 bis 4 treffen auf dieses Szenario nicht zu. Das Verspotten der externen Quelle beim Testen von fullname berücksichtigt alle diese Gründe für das Verspotten. Das einzige Stück, das nicht behandelt wird, ist die Einfachheit, aber es scheint, dass das Objekt so einfach ist, dass es kein Problem darstellt.

Ich denke, Ihr Anliegen ist Grund Nummer 5. Das Anliegen ist, dass eine Änderung der Implementierung von FirstName und LastName zu einem späteren Zeitpunkt den Test unterbrechen wird. Zukünftig können Vorname und Nachname die Namen von einem anderen Ort oder einer anderen Quelle erhalten. Aber FullName wird es wahrscheinlich immer seinFirstName() + " " + LastName() . Aus diesem Grund möchten Sie FullName testen, indem Sie FirstName und LastName verspotten.

Was Sie dann haben, ist eine Teilmenge des Personenobjekts, die sich mit größerer Wahrscheinlichkeit ändert als die anderen. Der Rest des Objekts verwendet diese Teilmenge. Diese Teilmenge ruft gegenwärtig ihre Daten mit einer Quelle ab, kann diese Daten jedoch zu einem späteren Zeitpunkt auf eine völlig andere Weise abrufen. Aber für mich klingt es so, als wäre diese Untermenge ein bestimmtes Objekt, das versucht, herauszukommen.

Es scheint mir, dass Sie das Objekt aufteilen, wenn Sie die Methode des Objekts verspotten. Aber Sie tun dies ad-hoc. Ihr Code macht nicht deutlich, dass sich in Ihrem Person-Objekt zwei unterschiedliche Teile befinden. Teilen Sie das Objekt einfach in Ihren eigentlichen Code auf, damit Sie beim Lesen Ihres Codes erkennen können, was gerade passiert. Wählen Sie die tatsächliche Aufteilung des Objekts, die Sinn macht, und versuchen Sie nicht, das Objekt für jeden Test unterschiedlich aufzuteilen.

Ich vermute, Sie haben etwas dagegen, Ihr Objekt aufzuteilen, aber warum?

BEARBEITEN

Ich lag falsch.

Sie sollten Objekte aufteilen, anstatt Ad-hoc-Teilungen durch Verspotten einzelner Methoden einzuführen. Ich habe mich jedoch zu sehr auf die eine Methode zum Teilen von Objekten konzentriert. OO bietet jedoch mehrere Methoden zum Teilen eines Objekts.

Was ich vorschlagen würde:

class PersonBase
{
      abstract sring FirstName();
      abstract string LastName();

      string FullName()
      {
            return FirstName() + " " + LastName();
      }
 }

 class Person extends PersonBase
 {
      string FirstName(); 
      string LastName();
 }

 class FakePerson extends PersonBase
 {
      void setFirstName(string);
      void setLastName(string);
      string getFirstName();
      string getLastName();
 }

Vielleicht haben Sie das die ganze Zeit so gemacht. Aber ich denke nicht, dass diese Methode die Probleme haben wird, die ich bei den Spottmethoden gesehen habe, da wir klar definiert haben, auf welcher Seite sich jede Methode befindet. Durch die Verwendung der Vererbung vermeiden wir die Unannehmlichkeiten, die durch die Verwendung eines zusätzlichen Wrapper-Objekts entstehen würden.

Dies bringt eine gewisse Komplexität mit sich, und für nur ein paar Dienstprogrammfunktionen würde ich sie wahrscheinlich nur testen, indem ich die zugrunde liegende Quelle eines Drittanbieters verspotte. Sicher, sie sind einer erhöhten Bruchgefahr ausgesetzt, aber es lohnt sich nicht, sie neu anzuordnen. Wenn Sie ein Objekt haben, das so komplex ist, dass Sie es teilen müssen, halte ich so etwas für eine gute Idee.

Winston Ewert
quelle
8
Gut gesagt. Wenn Sie versuchen, Problemumgehungen beim Testen zu finden, müssen Sie wahrscheinlich Ihr Design überdenken (als Randnotiz habe ich jetzt Frank Sinatras Stimme im Kopf).
Problematische
2
"Indem Sie die öffentlichen Methoden verspotten, um das Objekt zu testen, nehmen Sie an, [wie] dieses Objekt implementiert wird." Aber ist das nicht der Fall, wenn Sie einen Gegenstand stutzen? Angenommen, ich weiß, dass meine Methode xyz () ein anderes Objekt aufruft. Um xyz () isoliert zu testen, muss ich das andere Objekt stubben. Daher muss mein Test die Implementierungsdetails meiner xyz () -Methode kennen.
Benutzer
In meinem Fall sind die Methoden "FirstName ()" und "LastName ()" einfach, fragen jedoch eine Drittanbieter-API nach ihrem Ergebnis ab.
Benutzer
@User, aktualisiert, vermutlich verspotten Sie die Drittanbieter-API, um Vorname und Nachname zu testen. Was ist falsch daran, beim Testen von FullName dasselbe zu verspotten?
Winston Ewert
@ Winston, das ist meine ganze Sache. Momentan verspotte ich die 3rd-Party-API, die in Vor- und Nachname verwendet wird, um den vollständigen Namen () zu testen, aber es ist mir egal, wie Vor- und Nachname beim Testen des vollständigen Namens implementiert werden (natürlich) Es ist in Ordnung, wenn ich Vor- und Nachnamen teste. Daher meine Frage zum Verspotten von Vor- und Nachnamen.
Benutzer
10

Obwohl ich der Antwort von Winston Ewert zustimme, ist es manchmal einfach nicht möglich, eine Klasse zu trennen, sei es aus Zeitgründen, weil die API bereits an anderer Stelle verwendet wird oder weil Sie eine haben.

In einem solchen Fall würde ich anstelle von Spottmethoden meine Tests schreiben, um die Klasse schrittweise abzudecken. Testen Sie die Methoden getExpenses()und getRevenue()und anschließend die getProfit()Methoden und anschließend die gemeinsam genutzten Methoden. Es ist richtig, dass Sie mehr als einen Test für eine bestimmte Methode haben. Da Sie jedoch Tests geschrieben haben, um die Methoden einzeln zu behandeln, können Sie sicher sein, dass Ihre Ausgaben bei einer getesteten Eingabe zuverlässig sind.

Problematisch
quelle
1
Einverstanden. Aber nur um den Punkt für jeden zu betonen, der dies liest, dies ist die Lösung, die Sie verwenden, wenn Sie die bessere Lösung nicht machen können.
Winston Ewert
5
@ Winston, und dies ist die Lösung, die Sie verwenden, bevor Sie eine bessere Lösung finden. Wenn Sie also einen großen Ball an Legacy-Code haben, müssen Sie ihn mit Unit-Tests abdecken, bevor Sie ihn überarbeiten können.
Péter Török
@ Péter Török, guter Punkt. Obwohl ich denke, dass dies unter "kann die bessere Lösung nicht tun" behandelt wird. :)
Winston Ewert
@all Das Testen der Methode getProfit () ist sehr schwierig, da die verschiedenen Szenarien für getExpenses () und getRevenues () tatsächlich die für getProfit () erforderlichen Szenarien multiplizieren. Wenn wir getExpenses () und Revenues () unabhängig testen. Ist es nicht eine gute Idee, diese Methoden zum Testen von getProfit () zu verspotten?
Mohd Farid
7

Um Ihre Beispiele weiter zu vereinfachen, sagen Sie, Sie testen C(), was von A()und abhängt B(), von denen jedes seine eigenen Abhängigkeiten hat. IMO kommt es darauf an, was Ihr Test zu erreichen versucht.

Wenn Sie das Verhalten der zu test C()bei bekannten Verhalten A()und B()dann ist es wahrscheinlich am einfachsten und am besten Stub A()undB() . Dies würde wahrscheinlich von Puristen als Unit-Test bezeichnet.

Wenn Sie das Verhalten des gesamten Systems zu testen sind (aus C()‚s - Perspektive), würde ich verlassen A()und B()wie umgesetzt und entweder Stummel aus ihren Abhängigkeiten (wenn es macht es zu Test möglich) oder eine Sandbox - Umgebung, wie zum Beispiel eines Test einrichten Datenbank. Ich würde das einen Integrationstest nennen.

Beide Ansätze mögen gültig sein. Wenn es also so konzipiert ist, dass Sie es in beide Richtungen testen können, ist es auf lange Sicht wahrscheinlich besser.

Rebecca Scott
quelle