web-dev-qa-db-de.com

Wie testet man abstrakte Klassen: Mit Stichleitungen erweitern?

Ich habe mich gefragt, wie man abstrakte Klassen und Klassen, die abstrakte Klassen erweitern, einem Komponententest unterzieht.

Sollte ich die abstrakte Klasse testen, indem ich sie erweitere, die abstrakten Methoden herausstreiche und dann alle konkreten Methoden teste? Testen Sie dann nur die Methoden, die ich überschreibe, und testen Sie die abstrakten Methoden in den Unit-Tests für Objekte, die meine abstrakte Klasse erweitern.

Soll 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.

423
Paul Whelan

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

Sie sollten eine abstrakte Klasse testen, die wie alle anderen Klassen Logik enthält.

252

Abstrakte Basisklassen können auf zwei Arten 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 Entwurf auszugleichen, und Clients verwenden die konkreten Implementierungen über ihre eigenen Schnittstellen.


Lösung für 1 - Strategiemuster

Option1

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 zu konkretisieren und eine Instanz dieser Schnittstelle in ihrem Konstruktor zu übernehmen. Ihre abgeleiteten Klassen werden dann zu Implementierungen dieser neuen Schnittstelle.

IMotor

Dies bedeutet, dass Sie Ihre zuvor abstrakte Klasse jetzt mit 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

Werfen Sie einen Blick auf die darin enthaltenen Funktionen. Prüfen Sie, ob etwas davon auf die zu bearbeitenden Objekte geschoben werden kann, um diese Duplizierung zu minimieren. Wenn Sie noch etwas übrig haben, versuchen Sie, es zu einer Hilfsklasse zu machen, die Ihre konkrete Implementierung in ihren Konstruktor aufnimmt, und entfernen Sie ihre Basisklasse.

Motor Helper

Dies führt wiederum zu konkreten Klassen, die einfach und leicht überprüfbar 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 geht man mit Mischungen von beiden um?

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

Wenn Sie feststellen, dass einige Methoden von Ihrer Basisklasse direkt implementiert werden und andere virtuell sind, können Sie den Vererbungsbaum dennoch in ein Strategiemuster umwandeln. Ich würde es jedoch auch als guten Indikator dafür ansehen, dass die Verantwortlichkeiten nicht richtig ausgerichtet sind und möglicherweise müssen umgestalten.


pdate 2: Abstrakte Klassen als Sprungbrett (2014/06/12)

Ich hatte neulich eine Situation, in der ich abstrakt gesprochen habe, deshalb möchte ich gerne untersuchen, warum.

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

Ich implementierte dies, indem ich eine abstrakte Basisklasse hatte, die die Formate der Einstellungsdateien und abgeleitete Klassen analysieren kann, die dieselben Methoden verfügbar machten, aber den Speicherort der Einstellungsdatei einkapselten.

Ich hätte einen "SettingsFileParser" schreiben können, den die 3 Klassen umschlossen und dann an die Basisklasse delegierten, um die Datenzugriffsmethoden offenzulegen. Ich habe mich dazu entschieden noch nicht zu tun, da dies zu 3 abgeleiteten Klassen mit mehr Delegation führen würde Code in ihnen als alles andere.

Mit der Entwicklung dieses Codes werden jedoch die Konsumenten jeder dieser Einstellungsklassen klarer. Jeder Einstellungsbenutzer fragt nach einigen Einstellungen und wandelt sie auf irgendeine Weise um (da Einstellungen Text sind, können sie sie in Objekte einschließen, um sie in Zahlen usw. umzuwandeln). In diesem Fall beginne ich, 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 Einstellungssatz, von der letztendlich nicht mehr gewusst wird, 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 und Zugriffsmethoden enthält. Daher werde ich diese Klasse ändern, um eine Einstellungsparserklasse zu kapseln, anstatt sie abzuleiten.

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

Ich finde das mit jeder Regel wahr ... 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 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 ein neues Verhalten hinzufügen, ist es, als würde Regen auf Ihrem Code landen. Zuerst platzierst du es dort, wo es landet. Dann überarbeitest du es, damit die Kräfte des guten Designs das Verhalten herumschubsen können, bis alles in den Tälern endet.

423
Nigel Thorne

Was ich für abstrakte Klassen und Interfaces tue, 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 gesetzt. Diese Testklasse wird nicht der Testsuite hinzugefügt, sondern Unterklassen davon, die eine Setup-Methode haben, die die Variable auf eine konkrete Implementierung von X setzt. Auf diese Weise dupliziere ich den Testcode nicht. Die Unterklassen des nicht verwendeten Tests können bei Bedarf weitere Testmethoden hinzufügen.

11
Mnementh

Wenn Ihre abstrakte Klasse konkrete Funktionen mit geschäftlichem Wert enthält, teste ich sie normalerweise direkt, indem ich ein Test-Double erstelle, das die abstrakten Daten ausblendet, 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 die Verwendung des Musters Vorlagenmethode , z. B. wenn ich eine Art erweiterbares Framework erstelle, das von einem Drittanbieter verwendet wird. In diesem Fall definiert die abstrakte Klasse den zu testenden Algorithmus. Daher ist es sinnvoller, die abstrakte Basis zu testen als eine bestimmte Implementierung.

Ich halte es jedoch für wichtig, dass sich diese Tests nur auf die konkreten Implementierungen der realen Geschäftslogik konzentrieren ; Sie sollten nicht Unit-Test Implementierungsdetails der abstrakten Klasse, weil Sie mit spröden Tests enden.

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.

8
Guillaume

eine Möglichkeit besteht darin, einen abstrakten Testfall zu schreiben, der Ihrer abstrakten Klasse entspricht, und dann konkrete Testfälle zu schreiben, die Ihre abstrakten Testfälle unterteilen. Tun Sie dies für jede konkrete Unterklasse Ihrer ursprünglichen abstrakten Klasse (d. h. Ihre Testfallhierarchie spiegelt Ihre Klassenhierarchie wider). Siehe Testen einer Schnittstelle im Rezeptbuch von junit: http://safari.informit.com/9781932394238/ch02lev1sec6 .

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

6
Ray Tayek

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 Hilfsmethoden oder -klassen hinzu, 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? Dann lassen Sie Ihre Tests nur mit dieser Methode interagieren. Testen Sie die Methoden in Ihrer Basisklasse? Dann haben Sie wahrscheinlich nur für diese Klasse einen separaten Scheinwerfer und testen jede Methode einzeln mit so vielen Tests wie nötig.

4
casademora

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 benutze:

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 ausdrücken.

4
Will

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 von Ihnen beschriebenen abstrakten Methoden zu stubben, sofern die konkreten Methoden der abstrakten Klasse von den untergeordneten Klassen entkoppelt sind.

3
Jeb

Eine der Hauptmotive für die Verwendung einer abstrakten Klasse ist die Aktivierung des Polymorphismus in Ihrer Anwendung - d. H. Sie können zur Laufzeit eine andere Version ersetzen. Tatsächlich ist dies fast das Gleiche wie bei der Verwendung einer Schnittstelle, mit der Ausnahme, dass die abstrakte Klasse einige allgemeine Installationsanweisungen bereitstellt, die häufig als Vorlagenmuster bezeichnet werden.

Aus der Sicht der Einheitentests 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.

bearbeiten: RhinoMocks ist ein großartiges Mock-Test-Framework, das zur Laufzeit Mock-Objekte generieren kann, indem es dynamisch von Ihrer Klasse abgeleitet wird. Mit diesem Ansatz sparen Sie unzählige Stunden beim Handcodieren abgeleiteter Klassen.

2
bryanbcook

Wenn die abstrakte Klasse eine konkrete Methode enthält, sollten Sie dies meines Erachtens anhand dieses Beispiels 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.

       }

   }
2
shreeram banne

Möglicherweise möchten Sie die Basisfunktionalität einer abstrakten Klasse testen. Am besten erweitern Sie die Klasse, ohne Methoden zu überschreiben, und spotten mit minimalem Aufwand über die abstrakten Methoden.

2
Ace

Nach der Antwort von @ patrick-desjardins habe ich abstract und seine Implementierungsklasse zusammen mit @Test Wie 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;
    }
}

Wie Abstrakte Klassen können nicht instanziiert werden, aber sie können untergeordnet werden , konkrete Klasse DEF.Java ist 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"));
    }
}
1
Arpit

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 Verwirrungen zu vermeiden, beachten Sie, dass diese konkrete Testklasse kein Schein, sondern ein 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 Schnittstellenmember des Mocks - ob Methoden oder Eigenschaften - wird zum Testzeitpunkt bereitgestellt (wiederum mithilfe eines Mocking-Frameworks). Auf diese Weise vermeiden Sie die Kopplung der zu testenden Implementierung mit der Implementierung ihrer Abhängigkeiten (die alle ihre eigenen diskreten Tests haben sollten).
0
banduki