Wie teste ich eine abstrakte Klasse in Java mit JUnit?

87

Ich bin neu in Java-Tests mit JUnit. Ich muss mit Java arbeiten und möchte Unit-Tests verwenden.

Mein Problem ist: Ich habe eine abstrakte Klasse mit einigen abstrakten Methoden. Es gibt jedoch einige Methoden, die nicht abstrakt sind. Wie kann ich diese Klasse mit JUnit testen? Beispielcode (sehr einfach):

abstract class Car {

    public Car(int speed, int fuel) {
        this.speed = speed;
        this.fuel = fuel;
    }

    private int speed;
    private int fuel;

    abstract void drive();

    public int getSpeed() {
        return this.speed;
    }

    public int getFuel() {
        return this.fuel;
    }
}

Ich möchte testen getSpeed()und getFuel()funktionieren.

Eine ähnliche Frage zu diesem Problem gibt es hier , es wird jedoch kein JUnit verwendet.

Im JUnit-FAQ-Bereich habe ich diesen Link gefunden , aber ich verstehe nicht, was der Autor mit diesem Beispiel sagen möchte. Was bedeutet diese Codezeile?

public abstract Source getSource() ;
vasco
quelle
4
Unter stackoverflow.com/questions/1087339/… finden Sie zwei Lösungen mit Mockito.
ddso
Gibt es einen Vorteil, ein anderes Framework zum Testen zu lernen? Ist Mockito nur eine Erweiterung von jUnit oder ein völlig anderes Projekt?
Vasco
Mockito ersetzt JUnit nicht. Wie andere Mocking-Frameworks wird es zusätzlich zu einem Unit-Test-Framework verwendet und hilft Ihnen beim Erstellen von Mock-Objekten, die Sie in Ihren Testfällen verwenden können.
ddso
1
Sprachunabhängig: stackoverflow.com/questions/243274/…
Ciro Santilli 6 冠状 病 六四 事件 6

Antworten:

104

Wenn Sie keine konkreten Implementierungen der Klasse haben und die Methoden nicht staticden Sinn haben, sie zu testen? Wenn Sie eine konkrete Klasse haben, testen Sie diese Methoden als Teil der öffentlichen API der konkreten Klasse.

Ich weiß, was Sie denken "Ich möchte diese Methoden nicht immer wieder testen, deshalb habe ich die abstrakte Klasse erstellt", aber mein Gegenargument dazu ist, dass der Zweck von Komponententests darin besteht, Entwicklern Änderungen zu ermöglichen. Führen Sie die Tests aus und analysieren Sie die Ergebnisse. Ein Teil dieser Änderungen kann das Überschreiben der Methoden Ihrer abstrakten Klasse umfassen, sowohl protectedals auch public, was zu grundlegenden Verhaltensänderungen führen kann. Abhängig von der Art dieser Änderungen kann dies Auswirkungen darauf haben, wie Ihre Anwendung auf unerwartete, möglicherweise negative Weise ausgeführt wird. Wenn Sie eine gute Unit-Testing-Suite haben, sollten Probleme, die sich aus diesen Typen ergeben, zum Zeitpunkt der Entwicklung offensichtlich sein.

nsfyn55
quelle
16
100% Code-Abdeckung ist ein Mythos. Sie sollten genau genug Tests haben, um alle Ihre bekannten Hypothesen über das Verhalten Ihrer Anwendung abzudecken (vorzugsweise geschrieben, bevor Sie den Code la Test Driven Development schreiben). Ich arbeite derzeit in einem sehr gut funktionierenden TDD-Team und wir haben zum Zeitpunkt unseres letzten Builds nur eine Abdeckung von 63%, die alle während unserer Entwicklung geschrieben wurden. Ist das gut? Wer weiß?, aber ich würde es als Zeitverschwendung betrachten, zurück zu gehen und zu versuchen, das höher zu steigern.
nsfyn55
3
sicher. Einige würden argumentieren, dass dies eine Verletzung der guten TDD ist. Stellen Sie sich vor, Sie sind in einem Team. Sie gehen davon aus, dass die Methode endgültig ist, und setzen in keiner konkreten Implementierung Tests ein. Jemand entfernt den Modifikator und nimmt Änderungen vor, die einen ganzen Zweig der Vererbungshierarchie betreffen. Möchten Sie nicht, dass Ihre Testsuite das erfasst?
nsfyn55
31
Ich bin nicht einverstanden. Unabhängig davon, ob Sie in TDD arbeiten oder nicht, enthält die konkrete Methode Ihrer abstrakten Klasse Code. Daher sollten sie Tests enthalten (unabhängig davon, ob Unterklassen vorhanden sind oder nicht). Darüber hinaus testet Unit-Test in Java (normalerweise) Klassen. Daher gibt es wirklich keine Logik beim Testen von Methoden, die nicht Teil der Klasse, sondern ihrer Superklasse sind. Nach dieser Logik sollten wir keine Klasse in Java testen, außer Klassen ohne Unterklassen. In Bezug auf die Methoden, die überschrieben werden, fügen Sie genau dann einen Test hinzu, um die Änderungen / Ergänzungen des Tests der Unterklasse zu überprüfen.
Ethanfar
3
@ nsfyn55 Was wäre, wenn die konkreten Methoden wären final? Ich sehe keinen Grund, dieselbe Methode mehrmals zu testen, wenn sich die Implementierung nicht ändern kann
Dioxin
3
Sollten die Tests nicht auf die abstrakte Schnittstelle abzielen, damit wir sie für alle Implementierungen ausführen können? Wenn es nicht möglich ist, würden wir gegen Liskovs verstoßen, über die wir Bescheid wissen und die wir beheben möchten. Nur wenn die Implementierung einige erweiterte (kompatible) Funktionen hinzufügt, sollten wir einen spezifischen Komponententest dafür durchführen (und nur für diese zusätzlichen Funktionen).
Der
36

Erstellen Sie eine konkrete Klasse, die die abstrakte Klasse erbt, und testen Sie dann die Funktionen, die die konkrete Klasse von der abstrakten Klasse erbt.

Kevin Bowersox
quelle
Was würden Sie tun, wenn Sie 10 konkrete Klassen haben, die die abstrakte Klasse erweitern, und jede dieser konkreten Klassen nur 1 Methode implementieren würde? Nehmen wir an, dass die anderen 2 Methoden für jede dieser Klassen gleich sind, da sie in der Zusammenfassung implementiert sind Klasse? Mein Fall ist, dass ich die Tests für die abstrakte Klasse nicht in jede Unterklasse kopieren und einfügen möchte.
Narbengesicht
12

Mit der von Ihnen geposteten Beispielklasse scheint es nicht sinnvoll zu sein, getFuel()und zu testengetSpeed() da sie nur 0 zurückgeben können (es gibt keine Setter).

Unter der Annahme, dass dies nur ein vereinfachtes Beispiel zur Veranschaulichung war und Sie berechtigte Gründe haben, Methoden in der abstrakten Basisklasse zu testen (andere haben bereits auf die Auswirkungen hingewiesen), können Sie Ihren Testcode so einrichten, dass ein anonymer Code erstellt wird Unterklasse der Basisklasse, die nur Dummy-Implementierungen (No-Op-Implementierungen) für die abstrakten Methoden bereitstellt.

In Ihrem TestCasekönnten Sie zum Beispiel Folgendes tun:

c = new Car() {
       void drive() { };
   };

Testen Sie dann den Rest der Methoden, z.

public class CarTest extends TestCase
{
    private Car c;

    public void setUp()
    {
        c = new Car() {
            void drive() { };
        };
    }

    public void testGetFuel() 
    {
        assertEquals(c.getFuel(), 0);
    }

    [...]
}

(Dieses Beispiel basiert auf der JUnit3-Syntax. Für JUnit4 wäre der Code etwas anders, aber die Idee ist dieselbe.)

Grodriguez
quelle
Danke für die Antwort. Ja, mein Beispiel wurde vereinfacht (und nicht so gut). Nachdem ich alle Antworten hier gelesen hatte, schrieb ich eine Dummy-Klasse. Aber wie @ nsfyn55 in seiner Antwort schrieb, schreibe ich einen Test für jeden Nachkommen dieser abstrakten Klasse.
Vasco
9

Wenn Sie ohnehin eine Lösung benötigen (z. B. weil Sie zu viele Implementierungen der abstrakten Klasse haben und das Testen immer dieselben Prozeduren wiederholen würde), können Sie eine abstrakte Testklasse mit einer abstrakten Factory-Methode erstellen, die durch die Implementierung dieser Methode ausgeführt wird Testklasse. Dieses Beispiel funktioniert oder ich mit TestNG:

Die abstrakte Testklasse von Car:

abstract class CarTest {

// the factory method
abstract Car createCar(int speed, int fuel);

// all test methods need to make use of the factory method to create the instance of a car
@Test
public void testGetSpeed() {
    Car car = createCar(33, 44);
    assertEquals(car.getSpeed(), 33);
    ...

Implementierung von Car

class ElectricCar extends Car {

    private final int batteryCapacity;

    public ElectricCar(int speed, int fuel, int batteryCapacity) {
        super(speed, fuel);
        this.batteryCapacity = batteryCapacity;
    }

    ...

Unit Test Klasse ElectricCarTestder Klasse ElectricCar:

class ElectricCarTest extends CarTest {

    // implementation of the abstract factory method
    Car createCar(int speed, int fuel) {
        return new ElectricCar(speed, fuel, 0);
    }

    // here you cann add specific test methods
    ...
thomas.mc.work
quelle
5

Sie könnten so etwas tun

public abstract MyAbstractClass {

    @Autowire
    private MyMock myMock;        

    protected String sayHello() {
            return myMock.getHello() + ", " + getName();
    }

    public abstract String getName();
}

// this is your JUnit test
public class MyAbstractClassTest extends MyAbstractClass {

    @Mock
    private MyMock myMock;

    @InjectMocks
    private MyAbstractClass thiz = this;

    private String myName = null;

    @Override
    public String getName() {
        return myName;
    }

    @Test
    public void testSayHello() {
        myName = "Johnny"
        when(myMock.getHello()).thenReturn("Hello");
        String result = sayHello();
        assertEquals("Hello, Johnny", result);
    }
}
iil
quelle
4

Ich würde eine innere Klasse von jUnit erstellen, die von der abstrakten Klasse erbt. Dies kann instanziiert werden und Zugriff auf alle in der abstrakten Klasse definierten Methoden haben.

public class AbstractClassTest {
   public void testMethod() {
   ...
   }
}


class ConcreteClass extends AbstractClass {

}
Marting
quelle
3
Dies ist ein ausgezeichneter Rat. Es könnte jedoch durch ein Beispiel verbessert werden. Vielleicht ein Beispiel für die Klasse, die Sie beschreiben.
SDJMcHattie
2

Sie können eine anonyme Klasse instanziieren und diese Klasse dann testen.

public class ClassUnderTest_Test {

    private ClassUnderTest classUnderTest;

    private MyDependencyService myDependencyService;

    @Before
    public void setUp() throws Exception {
        this.myDependencyService = new MyDependencyService();
        this.classUnderTest = getInstance();
    }

    private ClassUnderTest getInstance() {
        return new ClassUnderTest() {    
            private ClassUnderTest init(
                    MyDependencyService myDependencyService
            ) {
                this.myDependencyService = myDependencyService;
                return this;
            }

            @Override
            protected void myMethodToTest() {
                return super.myMethodToTest();
            }
        }.init(myDependencyService);
    }
}

Beachten Sie, dass die Sichtbarkeit protectedfür die Eigenschaft myDependencyServiceder abstrakten Klasse gelten mussClassUnderTest .

Sie können diesen Ansatz auch gut mit Mockito kombinieren. Siehe hier .

Samuel
quelle
2

Meine Art, dies zu testen, ist in jedem Fall recht einfach abstractUnitTest.java. Ich erstelle einfach eine Klasse in der abstractUnitTest.java, die die abstrakte Klasse erweitert. Und testen Sie es so.

Haomin
quelle
0

Sie können nicht die gesamte abstrakte Klasse testen. In diesem Fall verfügen Sie über abstrakte Methoden. Dies bedeutet, dass diese von einer Klasse implementiert werden sollten, die eine bestimmte abstrakte Klasse erweitert.

In dieser Klasse muss der Programmierer den Quellcode schreiben, der für seine Logik bestimmt ist.

Mit anderen Worten, es macht keinen Sinn, abstrakte Klassen zu testen, da Sie das endgültige Verhalten nicht überprüfen können.

Wenn Sie über wichtige Funktionen verfügen, die sich nicht auf abstrakte Methoden in einer abstrakten Klasse beziehen, erstellen Sie einfach eine andere Klasse, in der die abstrakte Methode eine Ausnahme auslöst.

Damian Leszczyński - Vash
quelle
0

Optional können Sie eine abstrakte Testklasse erstellen, die die Logik innerhalb der abstrakten Klasse abdeckt, und diese für jeden Unterklassentest erweitern. Auf diese Weise können Sie sicherstellen, dass diese Logik für jedes Kind separat getestet wird.

Andrew Taran
quelle