Wie werden abstrakte Klassen getestet: mit Stubs erweitern?

446

Ich habe mich gefragt, wie man abstrakte Klassen und Klassen, die abstrakte Klassen erweitern, einheitlich testet.

Sollte ich die abstrakte Klasse testen, indem ich sie erweitere, die abstrakten Methoden auslösche und dann alle konkreten Methoden teste? Testen Sie dann nur die Methoden, die ich überschreibe, und testen Sie die abstrakten Methoden in den Komponententests für Objekte, die meine abstrakte Klasse erweitern.

Sollte ich einen abstrakten Testfall haben, mit dem die Methoden der abstrakten Klasse getestet werden können, und diese Klasse in meinem Testfall für Objekte erweitern, die die abstrakte Klasse erweitern?

Beachten Sie, dass meine abstrakte Klasse einige konkrete Methoden hat.

Paul Whelan
quelle

Antworten:

268

Schreiben Sie ein Mock-Objekt und verwenden Sie es nur zum Testen. Sie sind normalerweise sehr, sehr minimal (erben von der abstrakten Klasse) und nicht mehr. Dann können Sie in Ihrem Unit Test die abstrakte Methode aufrufen, die Sie testen möchten.

Sie sollten abstrakte Klassen testen, die wie alle anderen Klassen eine Logik enthalten.

Patrick Desjardins
quelle
9
Verdammt, ich muss sagen, dass dies das erste Mal ist, dass ich der Idee eines Mocks zustimme.
Jonathan Allen
5
Sie benötigen zwei Klassen, einen Mock und einen Test. Die Scheinklasse erweitert nur die abstrakten Methoden der zu testenden abstrakten Klasse. Diese Methoden können no-op sein, null zurückgeben usw., da sie nicht getestet werden. Die Testklasse testet nur die nicht abstrakte öffentliche API (dh die von der Abstract-Klasse implementierte Schnittstelle). Für jede Klasse, die die Abstract-Klasse erweitert, benötigen Sie zusätzliche Testklassen, da die abstrakten Methoden nicht behandelt wurden.
Cyber-Mönch
10
Es ist offensichtlich möglich, dies zu tun. Um jedoch wirklich jede abgeleitete Klasse zu testen, werden Sie diese Basisfunktionalität immer wieder testen. Dies führt dazu, dass Sie eine abstrakte Testvorrichtung haben, damit Sie diese Duplizierung beim Testen herausrechnen können. Das alles riecht! Ich empfehle dringend, einen weiteren Blick darauf zu werfen, warum Sie überhaupt abstrakte Klassen verwenden, und zu prüfen, ob etwas anderes besser funktioniert.
Nigel Thorne
5
Die nächste Antwort ist viel besser.
Martin Spamer
22
@MartiSpamer: Ich würde nicht sagen, dass diese Antwort überbewertet ist, da sie viel früher (2 Jahre) geschrieben wurde als die Antwort, die Sie unten für besser halten. Lassen Sie uns Patrick einfach ermutigen, denn im Zusammenhang mit der Veröffentlichung dieser Antwort war es großartig. Lassen Sie uns einander ermutigen. Prost
Marvin Thobejane
449

Es gibt zwei Möglichkeiten, wie abstrakte Basisklassen verwendet werden.

  1. Sie spezialisieren Ihr abstraktes Objekt, aber alle Clients verwenden die abgeleitete Klasse über ihre Basisschnittstelle.

  2. Sie verwenden eine abstrakte Basisklasse, um die Duplizierung innerhalb von Objekten in Ihrem Design herauszufiltern, und Clients verwenden die konkreten Implementierungen über ihre eigenen Schnittstellen.!


Lösung für 1 - Strategiemuster

Option 1

Wenn Sie die erste Situation haben, haben Sie tatsächlich eine Schnittstelle, die durch die virtuellen Methoden in der abstrakten Klasse definiert ist, die Ihre abgeleiteten Klassen implementieren.

Sie sollten in Betracht ziehen, dies zu einer echten Schnittstelle zu machen, Ihre abstrakte Klasse konkret zu ändern und eine Instanz dieser Schnittstelle in ihren Konstruktor aufzunehmen. Ihre abgeleiteten Klassen werden dann zu Implementierungen dieser neuen Schnittstelle.

IMotor

Dies bedeutet, dass Sie Ihre zuvor abstrakte Klasse jetzt mithilfe einer Scheininstanz der neuen Schnittstelle und jeder neuen Implementierung über die jetzt öffentliche Schnittstelle testen können. Alles ist einfach und überprüfbar.


Lösung für 2

Wenn Sie die zweite Situation haben, arbeitet Ihre abstrakte Klasse als Hilfsklasse.

AbstractHelper

Schauen Sie sich die darin enthaltenen Funktionen an. Überprüfen Sie, ob etwas davon auf die Objekte verschoben werden kann, die bearbeitet werden, um diese Duplizierung zu minimieren. Wenn Sie noch etwas übrig haben, versuchen Sie, es zu einer Hilfsklasse zu machen, die Ihre konkrete Implementierung in ihrem Konstruktor übernimmt, und entfernen Sie ihre Basisklasse.

Motorhelfer

Dies führt wiederum zu konkreten Klassen, die einfach und leicht zu testen sind.


Als Regel

Bevorzugen Sie ein komplexes Netzwerk einfacher Objekte gegenüber einem einfachen Netzwerk komplexer Objekte.

Der Schlüssel zu erweiterbarem testbarem Code sind kleine Bausteine ​​und unabhängige Verkabelung.


Aktualisiert: Wie gehe ich mit Gemischen von beiden um?

Es ist möglich, dass eine Basisklasse beide Rollen ausführt ... dh: Sie verfügt über eine öffentliche Schnittstelle und geschützte Hilfsmethoden. Wenn dies der Fall ist, können Sie die Hilfsmethoden in eine Klasse zerlegen (Szenario 2) und den Vererbungsbaum in ein Strategiemuster konvertieren.

Wenn Sie feststellen, dass Sie einige Methoden haben, die Ihre Basisklasse direkt implementiert, und andere virtuell sind, können Sie den Vererbungsbaum dennoch in ein Strategiemuster konvertieren. Ich würde dies jedoch auch als guten Indikator dafür ansehen, dass die Verantwortlichkeiten nicht korrekt ausgerichtet sind und möglicherweise brauchen Refactoring.


Update 2: Abstract Classes als Sprungbrett (12.06.2014)

Ich hatte neulich eine Situation, in der ich abstrakt verwendet habe, deshalb möchte ich herausfinden, warum.

Wir haben ein Standardformat für unsere Konfigurationsdateien. Dieses spezielle Tool verfügt über 3 Konfigurationsdateien in diesem Format. Ich wollte eine stark typisierte Klasse für jede Einstellungsdatei, damit eine Klasse durch Abhängigkeitsinjektion nach den Einstellungen fragen kann, die sie interessiert.

Ich habe dies implementiert, indem ich eine abstrakte Basisklasse hatte, die weiß, wie die Einstellungsdateiformate und abgeleiteten Klassen analysiert werden, die dieselben Methoden verfügbar gemacht haben, aber den Speicherort der Einstellungsdatei gekapselt haben.

Ich hätte einen "SettingsFileParser" schreiben können, den die drei Klassen umschlossen und dann an die Basisklasse delegiert haben, um die Datenzugriffsmethoden verfügbar zu machen. Ich habe mich entschieden, dies noch nicht zu tun, da dies zu 3 abgeleiteten Klassen mit mehr Delegierungscode als alles andere führen würde.

Jedoch ... wenn sich dieser Code weiterentwickelt und die Verbraucher jeder dieser Einstellungsklassen klarer werden. Bei jedem Einstellungsbenutzer werden einige Einstellungen angefordert und auf irgendeine Weise transformiert (da es sich bei den Einstellungen um Text handelt, können sie in Objekte eingeschlossen werden, um sie in Zahlen usw. umzuwandeln). In diesem Fall werde ich beginnen, diese Logik in Datenmanipulationsmethoden zu extrahieren und sie wieder auf die stark typisierten Einstellungsklassen zu übertragen. Dies führt zu einer übergeordneten Benutzeroberfläche für jeden Satz von Einstellungen, die schließlich nicht mehr weiß, dass es sich um "Einstellungen" handelt.

Zu diesem Zeitpunkt benötigen die stark typisierten Einstellungsklassen nicht mehr die "Getter" -Methoden, die die zugrunde liegende Implementierung der Einstellungen verfügbar machen.

Zu diesem Zeitpunkt möchte ich nicht mehr, dass die öffentliche Schnittstelle die Einstellungen für die Zugriffsmethoden enthält. Daher werde ich diese Klasse ändern, um eine Parser-Klasse für Einstellungen zu kapseln, anstatt sie abzuleiten.

Die Abstract-Klasse ist daher: eine Möglichkeit für mich, im Moment Delegierungscode zu vermeiden, und eine Markierung im Code, die mich daran erinnert, das Design später zu ändern. Ich werde es vielleicht nie erreichen, also kann es eine Weile dauern ... nur der Code kann es sagen.

Ich finde, dass dies mit jeder Regel zutrifft ... wie "keine statischen Methoden" oder "keine privaten Methoden". Sie weisen auf einen Geruch im Code hin ... und das ist gut so. Es hält Sie auf der Suche nach der Abstraktion, die Sie verpasst haben ... und ermöglicht es Ihnen, Ihrem Kunden in der Zwischenzeit weiterhin einen Mehrwert zu bieten.

Ich stelle mir Regeln wie diese vor, die eine Landschaft definieren, in der wartbarer Code in den Tälern lebt. Wenn Sie neues Verhalten hinzufügen, ist es, als würde Regen auf Ihrem Code landen. Zuerst platzieren Sie es dort, wo es landet. Dann überarbeiten Sie es, damit die Kräfte guten Designs das Verhalten herumschieben können, bis alles in den Tälern landet.

Nigel Thorne
quelle
18
Dies ist eine großartige Antwort. Viel besser als die am besten bewerteten. Aber dann denke ich, dass nur diejenigen, die wirklich testbaren Code schreiben wollen, es schätzen würden .. :)
MalcomTucker
22
Ich kann nicht darüber hinwegkommen, wie gut diese Antwort tatsächlich ist. Es hat meine Denkweise über abstrakte Klassen total verändert. Danke Nigel.
MalcomTucker
4
Oh nein ... ein weiterer Grundsatz, den ich überdenken muss! Danke (sowohl sarkastisch für jetzt als auch nicht sarkastisch für einmal, ich habe es aufgenommen und fühle mich wie ein besserer Programmierer)
Martin Lyne
11
Gute Antwort. Auf jeden Fall etwas zum Nachdenken ... aber läuft das, was Sie sagen, nicht darauf hinaus, keine abstrakten Klassen zu verwenden?
Brianestey
32
+1 für die Regel allein: "Bevorzugen Sie ein komplexes Netzwerk einfacher Objekte gegenüber einem einfachen Netzwerk komplexer Objekte."
David Glass
12

Was ich für abstrakte Klassen und Schnittstellen mache, ist Folgendes: Ich schreibe einen Test, der das Objekt so verwendet, wie es konkret ist. Die Variable vom Typ X (X ist die abstrakte Klasse) wird im Test jedoch nicht festgelegt. Diese Testklasse wird nicht zur Testsuite hinzugefügt, sondern zu Unterklassen davon, die über eine Setup-Methode verfügen, mit der die Variable auf eine konkrete Implementierung von X gesetzt wird. Auf diese Weise dupliziere ich den Testcode nicht. Die Unterklassen des nicht verwendeten Tests können bei Bedarf weitere Testmethoden hinzufügen.

Mnementh
quelle
Verursacht dies nicht Casting-Probleme in der Unterklasse? Wenn X die Methode a hat und Y X erbt, aber auch die Methode b. Wenn Sie Ihre Testklasse unterordnen, müssen Sie Ihre abstrakte Variable nicht in ein Y umwandeln, um Tests für b durchzuführen?
Johnno Nolan
8

Um einen Komponententest speziell für die abstrakte Klasse durchzuführen, sollten Sie ihn zu Testzwecken ableiten, die Ergebnisse von base.method () und das beabsichtigte Verhalten beim Erben testen.

Sie testen eine Methode, indem Sie sie aufrufen. Testen Sie also eine abstrakte Klasse, indem Sie sie implementieren ...


quelle
8

Wenn Ihre abstrakte Klasse konkrete Funktionen enthält, die geschäftlichen Wert haben, teste ich sie normalerweise direkt, indem ich ein Testdouble erstelle, das die abstrakten Daten ausstößt, oder indem ich ein spöttisches Framework verwende, um dies für mich zu tun. Welche ich wähle, hängt stark davon ab, ob ich testspezifische Implementierungen der abstrakten Methoden schreiben muss oder nicht.

Das häufigste Szenario, in dem ich dies tun muss, ist, wenn ich das Muster der Vorlagenmethode verwende , z. B. wenn ich eine Art erweiterbares Framework erstelle, das von einem Drittanbieter verwendet wird. In diesem Fall definiert die abstrakte Klasse den Algorithmus, den ich testen möchte. Daher ist es sinnvoller, die abstrakte Basis zu testen als eine bestimmte Implementierung.

Ich denke jedoch, dass es wichtig ist, dass sich diese Tests nur auf die konkreten Implementierungen der realen Geschäftslogik konzentrieren . Sie sollten keine Details zur Implementierung von Unit-Tests für die abstrakte Klasse ausführen, da dies zu spröden Tests führen kann.

Seth Petry-Johnson
quelle
6

Eine Möglichkeit besteht darin, einen abstrakten Testfall zu schreiben, der Ihrer abstrakten Klasse entspricht, und dann konkrete Testfälle zu schreiben, die Ihren abstrakten Testfall unterordnen. Führen Sie dies für jede konkrete Unterklasse Ihrer ursprünglichen abstrakten Klasse aus (dh Ihre Testfallhierarchie spiegelt Ihre Klassenhierarchie wider). Siehe Testen einer Schnittstelle im Junit-Rezeptbuch: http://safari.informit.com/9781932394238/ch02lev1sec6 .

Siehe auch Testcase Superclass in xUnit-Mustern: http://xunitpatterns.com/Testcase%20Superclass.html

Ray Tayek
quelle
4

Ich würde gegen "abstrakte" Tests argumentieren. Ich denke, ein Test ist eine konkrete Idee und hat keine Abstraktion. Wenn Sie gemeinsame Elemente haben, fügen Sie diese in Hilfsmethoden oder -klassen ein, die jeder verwenden kann.

Stellen Sie beim Testen einer abstrakten Testklasse sicher, dass Sie sich fragen, was Sie testen. Es gibt verschiedene Ansätze, und Sie sollten herausfinden, was in Ihrem Szenario funktioniert. Versuchen Sie, eine neue Methode in Ihrer Unterklasse zu testen? Lassen Sie dann Ihre Tests nur mit dieser Methode interagieren. Testen Sie die Methoden in Ihrer Basisklasse? Dann haben Sie wahrscheinlich nur für diese Klasse ein separates Gerät und testen Sie jede Methode einzeln mit so vielen Tests wie nötig.

casademora
quelle
Ich wollte den Code, den ich bereits getestet hatte, nicht erneut testen, deshalb ging ich den Weg des abstrakten Testfalls. Ich versuche, alle konkreten Methoden in meiner abstrakten Klasse an einem Ort zu testen.
Paul Whelan
7
Ich bin nicht damit einverstanden, gemeinsame Elemente für Hilfsklassen zu extrahieren, zumindest in einigen (vielen?) Fällen. Wenn eine abstrakte Klasse konkrete Funktionen enthält, ist es meiner Meinung nach durchaus akzeptabel, diese Funktionen direkt zu testen.
Seth Petry-Johnson
4

Dies ist das Muster, dem ich normalerweise folge, wenn ich ein Geschirr zum Testen einer abstrakten Klasse einrichte:

public abstract class MyBase{
  /*...*/
  public abstract void VoidMethod(object param1);
  public abstract object MethodWithReturn(object param1);
  /*,,,*/
}

Und die Version, die ich im Test verwende:

public class MyBaseHarness : MyBase{
  /*...*/
  public Action<object> VoidMethodFunction;
  public override void VoidMethod(object param1){
    VoidMethodFunction(param1);
  }
  public Func<object, object> MethodWithReturnFunction;
  public override object MethodWithReturn(object param1){
    return MethodWihtReturnFunction(param1);
  }
  /*,,,*/
}

Wenn die abstrakten Methoden aufgerufen werden, wenn ich es nicht erwarte, schlagen die Tests fehl. Beim Anordnen der Tests kann ich die abstrakten Methoden mit Lambdas, die Asserts ausführen, Ausnahmen auslösen, unterschiedliche Werte zurückgeben usw., leicht ausmerzen.


quelle
3

Wenn die konkreten Methoden eine der abstrakten Methoden aufrufen, funktioniert diese Strategie nicht und Sie möchten das Verhalten jeder untergeordneten Klasse separat testen. Andernfalls sollte es in Ordnung sein, es zu erweitern und die abstrakten Methoden wie beschrieben zu stubben, vorausgesetzt, die konkreten Methoden der abstrakten Klasse sind von untergeordneten Klassen entkoppelt.

Jeb
quelle
2

Ich nehme an, Sie möchten die Basisfunktionalität einer abstrakten Klasse testen ... Aber am besten erweitern Sie die Klasse, ohne Methoden zu überschreiben, und verspotten die abstrakten Methoden mit minimalem Aufwand.

As
quelle
2

Eine der Hauptmotive für die Verwendung einer abstrakten Klasse besteht darin, Polymorphismus in Ihrer Anwendung zu aktivieren - dh Sie können zur Laufzeit eine andere Version ersetzen. Tatsächlich ist dies fast dasselbe wie die Verwendung einer Schnittstelle, mit der Ausnahme, dass die abstrakte Klasse einige gängige Installationsfunktionen bereitstellt, die häufig als Vorlagenmuster bezeichnet werden .

Aus Sicht der Unit-Tests sind zwei Dinge zu beachten:

  1. Interaktion Ihrer abstrakten Klasse mit verwandten Klassen . Die Verwendung eines Mock-Test-Frameworks ist ideal für dieses Szenario, da es zeigt, dass Ihre abstrakte Klasse gut mit anderen zusammenarbeitet.

  2. Funktionalität abgeleiteter Klassen . Wenn Sie eine benutzerdefinierte Logik haben, die Sie für Ihre abgeleiteten Klassen geschrieben haben, sollten Sie diese Klassen isoliert testen.

edit: RhinoMocks ist ein fantastisches Mock-Test-Framework, das zur Laufzeit Mock-Objekte generieren kann, indem es dynamisch von Ihrer Klasse abgeleitet wird. Mit diesem Ansatz können Sie unzählige Stunden an handcodierten abgeleiteten Klassen sparen.

bryanbcook
quelle
2

Erstens, wenn die abstrakte Klasse eine konkrete Methode enthielt, sollten Sie dies in diesem Beispiel tun

 public abstract class A 

 {

    public boolean method 1
    {
        // concrete method which we have to test.

    }


 }


 class B extends class A

 {

      @override
      public boolean method 1
      {
        // override same method as above.

      }


 } 


  class Test_A 

  {

    private static B b;  // reference object of the class B

    @Before
    public void init()

      {

      b = new B ();    

      }

     @Test
     public void Test_method 1

       {

       b.method 1; // use some assertion statements.

       }

   }
Shreeram Banne
quelle
1

Wenn eine abstrakte Klasse für Ihre Implementierung geeignet ist, testen Sie (wie oben vorgeschlagen) eine abgeleitete konkrete Klasse. Ihre Annahmen sind richtig.

Um zukünftige Verwirrung zu vermeiden, beachten Sie, dass diese konkrete Testklasse kein Schein, sondern eine Fälschung ist .

Streng genommen wird ein Mock durch die folgenden Merkmale definiert:

  • Anstelle jeder Abhängigkeit der zu testenden Fachklasse wird ein Mock verwendet.
  • Ein Mock ist eine Pseudoimplementierung einer Schnittstelle (Sie können sich daran erinnern, dass Abhängigkeiten in der Regel als Schnittstellen deklariert werden sollten; Testbarkeit ist ein Hauptgrund dafür).
  • Das Verhalten der Mitglieder der Mock-Schnittstelle - ob Methoden oder Eigenschaften - wird zur Testzeit angegeben (wiederum mithilfe eines Mocking-Frameworks). Auf diese Weise vermeiden Sie die Kopplung der getesteten Implementierung mit der Implementierung ihrer Abhängigkeiten (die alle ihre eigenen diskreten Tests haben sollten).
Banduki
quelle
1

Nach der Antwort von @ patrick-desjardins habe ich abstract und seine Implementierungsklasse @Testwie folgt implementiert :

Abstrakte Klasse - ABC.java

import java.util.ArrayList;
import java.util.List;

public abstract class ABC {

    abstract String sayHello();

    public List<String> getList() {
        final List<String> defaultList = new ArrayList<>();
        defaultList.add("abstract class");
        return defaultList;
    }
}

Da abstrakte Klassen nicht instanziiert, aber in Unterklassen unterteilt werden können , lautet die konkrete Klasse DEF.java wie folgt:

public class DEF extends ABC {

    @Override
    public String sayHello() {
        return "Hello!";
    }
}

@ Test-Klasse zum Testen sowohl abstrakter als auch nicht abstrakter Methoden:

import org.junit.Before;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.contains;
import java.util.Collection;
import java.util.List;
import static org.hamcrest.Matchers.equalTo;

import org.junit.Test;

public class DEFTest {

    private DEF def;

    @Before
    public void setup() {
        def = new DEF();
    }

    @Test
    public void add(){
        String result = def.sayHello();
        assertThat(result, is(equalTo("Hello!")));
    }

    @Test
    public void getList(){
        List<String> result = def.getList();
        assertThat((Collection<String>) result, is(not(empty())));
        assertThat(result, contains("abstract class"));
    }
}
Arpit Aggarwal
quelle