Wie schreibe ich Junit-Tests für Schnittstellen?

77

Was ist der beste Weg, um Junit-Tests für Schnittstellen zu schreiben, damit sie für die konkreten Implementierungsklassen verwendet werden können?

zB Sie haben diese Schnittstelle und implementieren Klassen:

public interface MyInterface {
    /** Return the given value. */
    public boolean myMethod(boolean retVal);
}

public class MyClass1 implements MyInterface {
    public boolean myMethod(boolean retVal) {
        return retVal;
    }
}

public class MyClass2 implements MyInterface {
    public boolean myMethod(boolean retVal) {
        return retVal;
    }
}

Wie würden Sie einen Test für die Schnittstelle schreiben, damit Sie ihn für die Klasse verwenden können?

Möglichkeit 1:

public abstract class MyInterfaceTest {
    public abstract MyInterface createInstance();

    @Test
    public final void testMyMethod_True() {
        MyInterface instance = createInstance();
        assertTrue(instance.myMethod(true));
    }

    @Test
    public final void testMyMethod_False() {
        MyInterface instance = createInstance();
        assertFalse(instance.myMethod(false));
    }
}

public class MyClass1Test extends MyInterfaceTest {
    public MyInterface createInstance() {
        return new MyClass1();
    }
}

public class MyClass2Test extends MyInterfaceTest {
    public MyInterface createInstance() {
        return new MyClass2();
    }
}

Profi:

  • Es muss nur eine Methode implementiert werden

Con:

  • Abhängigkeiten und Scheinobjekte der zu testenden Klasse müssen für alle Tests gleich sein

Möglichkeit 2:

public abstract class MyInterfaceTest
    public void testMyMethod_True(MyInterface instance) {
        assertTrue(instance.myMethod(true));
    }

    public void testMyMethod_False(MyInterface instance) {
        assertFalse(instance.myMethod(false));
    }
}

public class MyClass1Test extends MyInterfaceTest {
    @Test
    public void testMyMethod_True() {
        MyClass1 instance = new MyClass1();
        super.testMyMethod_True(instance);
    }

    @Test
    public void testMyMethod_False() {
        MyClass1 instance = new MyClass1();
        super.testMyMethod_False(instance);
    }
}

public class MyClass2Test extends MyInterfaceTest {
    @Test
    public void testMyMethod_True() {
        MyClass1 instance = new MyClass2();
        super.testMyMethod_True(instance);
    }

    @Test
    public void testMyMethod_False() {
        MyClass1 instance = new MyClass2();
        super.testMyMethod_False(instance);
    }
}

Profi:

  • Feinabstimmung für jeden Test, einschließlich Abhängigkeiten und Scheinobjekte

Con:

  • Jede implementierende Testklasse muss zusätzliche Testmethoden schreiben

Welche Möglichkeit würden Sie bevorzugen oder welchen anderen Weg nutzen Sie?

Xeno Lupus
quelle
Möglichkeit 1 ist nicht ausreichend, wenn sich die konkrete Klasse in einem anderen Paket, einer anderen Komponente oder einem anderen Entwicklungsteam befindet.
Andy Thomas
1
@AndyThomas: Warum sagst du das? Ich benutze Möglichkeit 1 mit konkreten Klassen (sowohl für die Implementierungen als auch für Tests) in verschiedenen Paketen und Maven-Projekten.
Trevor Robinson
1
@TrevorRobinson - Wenn ich an diesen drei Jahre alten Kommentar zurückdenke, kann ich mir im Moment nur vorstellen, dass Klassen außerhalb Ihrer Kontrolle möglicherweise mehrere Konstruktoren haben, aber Möglichkeit 1 führt jeden Test für ein Objekt aus, das nur mit einem dieser erstellt wurde.
Andy Thomas
Mit Option1 können Sie in jeder konkreten Klasse separate @ Vor-Methoden verwenden. Sie können auch einmalige Tests in konkreten Klassen durchführen lassen, wie Sie es für richtig halten.
JoshOrndorff
Testanmerkungen in Schnittstellen sind jetzt in JUnit 5 möglich und funktionieren wie erwartet.
Steven Jeuris

Antworten:

79

Im Gegensatz zu der vielfach abgestimmten Antwort, die @dlev gegeben hat, kann es manchmal sehr nützlich / notwendig sein, einen Test zu schreiben, wie Sie ihn vorschlagen. Die öffentliche API einer Klasse, ausgedrückt durch ihre Schnittstelle, ist das Wichtigste, was getestet werden muss. Davon abgesehen würde ich keinen der von Ihnen genannten Ansätze verwenden, sondern stattdessen einen parametrisierten Test, bei dem die Parameter die zu testenden Implementierungen sind:

@RunWith(Parameterized.class)
public class InterfaceTesting {
    public MyInterface myInterface;

    public InterfaceTesting(MyInterface myInterface) {
        this.myInterface = myInterface;
    }

    @Test
    public final void testMyMethod_True() {
        assertTrue(myInterface.myMethod(true));
    }

    @Test
    public final void testMyMethod_False() {
        assertFalse(myInterface.myMethod(false));
    }

    @Parameterized.Parameters
    public static Collection<Object[]> instancesToTest() {
        return Arrays.asList(
                    new Object[]{new MyClass1()},
                    new Object[]{new MyClass2()}
        );
    }
}
Ryan Stewart
quelle
4
Bei diesem Ansatz scheint es ein Problem zu geben. Die gleiche Instanz von MyClass1 und MyClass2 wird verwendet, um alle Testmethoden auszuführen. Im Idealfall sollte jede Testmethode mit einer neuen Instanz von MyClass1 / MyClass2 ausgeführt werden. Dieser Nachteil macht diesen Ansatz nicht verwendbar.
ChrisOdney
12
Wenn Sie für jede Testmethode eine neue Fixture-Instanz benötigen, lassen Sie die Parameters-Methode eine Factory zurückgeben, die jeder Test aufruft, um das Fixture abzurufen. Dies hat keinen Einfluss auf die Realisierbarkeit dieses Ansatzes.
Ryan Stewart
1
Was ist mit dem Platzieren von Klassenreferenzen auf Parameter und dem Instanziieren in der "InterfaceTesting" -Methode mit Reflexion?
ArcanisCz
2
@ArcanisCz: Wie beim Eariler-Kommentator ist es nicht so wichtig , wie Sie die Instanz zum Testen bringen. Der wichtige Punkt ist, dass eine Art parametrisierter Test wahrscheinlich der richtige Ansatz ist.
Ryan Stewart
4
Angenommen, ich schreibe eine Schnittstelle, stelle einige Implementierungen und einen so geschriebenen Test bereit. Wenn ein Benutzer eine neue Implementierung erstellt und diese testen möchte, muss er den Quellcode des Tests ändern. Aus diesem Grund halte ich die abstrakte Methode, die die Testinstanz zurückgibt, für nützlicher, insbesondere wenn Sie erwarten, dass Ihre Clients ihre eigenen Implementierungen erstellen.
Jspurim
20

Ich bin mit @dlev überhaupt nicht einverstanden. Sehr oft ist es eine sehr gute Praxis, Tests zu schreiben, die Schnittstellen verwenden. Die Schnittstelle definiert den Vertrag zwischen dem Kunden und der Implementierung. Sehr oft müssen alle Ihre Implementierungen genau die gleichen Tests bestehen. Natürlich kann jede Implementierung ihre eigenen Tests haben.

Ich kenne also 2 Lösungen.

  1. Implementieren Sie einen abstrakten Testfall mit verschiedenen Tests, die die Schnittstelle verwenden. Deklarieren Sie eine abstrakte geschützte Methode, die eine konkrete Instanz zurückgibt. Erben Sie diese abstrakte Klasse nun so oft, wie Sie für jede Implementierung Ihrer Schnittstelle benötigen, und implementieren Sie die erwähnte Factory-Methode entsprechend. Sie können hier auch spezifischere Tests hinzufügen.

  2. Verwenden Sie Testsuiten .

AlexR
quelle
14

Ich bin auch nicht mit dlev einverstanden, es ist nichts Falsches daran, Ihre Tests gegen Schnittstellen zu schreiben, anstatt konkrete Implementierungen.

Sie möchten wahrscheinlich parametrisierte Tests verwenden. So würde es mit TestNG aussehen , mit JUnit ist es etwas ausgefeilter (da Sie Parameter nicht direkt an Testfunktionen übergeben können):

@DataProvider
public Object[][] dp() {
  return new Object[][] {
    new Object[] { new MyImpl1() },
    new Object[] { new MyImpl2() },
  }
}

@Test(dataProvider = "dp")
public void f(MyInterface itf) {
  // will be called, with a different implementation each time
}
Cedric Beust
quelle
Gute Antwort. Sieht so aus, als würde man einen schönen Mechanismus dafür testen.
Fastcodejava
13

Späte Ergänzung des Themas, Austausch neuer Lösungserkenntnisse

Ich suche auch nach einer geeigneten und effizienten Methode zum Testen (basierend auf JUnit) der Korrektheit mehrerer Implementierungen einiger Schnittstellen und abstrakter Klassen. Leider entsprechen weder die @ParameterizedTests von JUnit noch das entsprechende Konzept von TestNG meinen Anforderungen, da ich die Liste der Implementierungen dieser möglicherweise vorhandenen Schnittstellen- / abstrakten Klassen a priori nicht kenne . Das heißt, neue Implementierungen werden möglicherweise entwickelt, und Tester haben möglicherweise nicht Zugriff auf alle vorhandenen Implementierungen. Es ist daher nicht effizient, wenn Testklassen die Liste der Implementierungsklassen angeben.

Zu diesem Zeitpunkt habe ich das folgende Projekt gefunden, das eine vollständige und effiziente Lösung zur Vereinfachung dieser Art von Tests zu bieten scheint: https://github.com/Claudenw/junit-contracts . Grundsätzlich ermöglicht es die Definition von "Vertragstests" durch die Anmerkung @Contract(InterfaceClass.class)zu Vertragstestklassen. Dann würde ein Implementierer eine implementierungsspezifische Testklasse mit Anmerkungen @RunWith(ContractSuite.class)und erstellen @ContractImpl(value = ImplementationClass.class). Die Engine wendet automatisch alle Vertragstests an, die für ImplementationClass gelten, indem sie nach allen Vertragstests sucht, die für eine Schnittstelle oder abstrakte Klasse definiert sind, von der ImplementationClass abgeleitet ist. Ich habe diese Lösung noch nicht getestet, aber das klingt vielversprechend.

Ich habe auch die folgende Bibliothek gefunden: http://www.jqno.nl/equalsverifier/ . Dieser erfüllt ein ähnliches, wenn auch viel spezifischeres Bedürfnis, das eine Klassenkonformität speziell für Object.equals- und Object.hashcode-Verträge behauptet.

In ähnlicher Weise zeigt https://bitbucket.org/chas678/testhelpers/src eine Strategie zur Validierung einiger Java-Fondamental-Verträge, einschließlich Object.equals, Object.hashcode, Comparable.compare, Serializable. Dieses Projekt verwendet einfache Teststrukturen, die meiner Meinung nach leicht reproduziert werden können, um spezifischen Anforderungen gerecht zu werden.

Nun, das ist es für jetzt; Ich werde diesen Beitrag mit anderen nützlichen Informationen, die ich möglicherweise finde, auf dem Laufenden halten.

Jwatkins
quelle
6

Ich würde es im Allgemeinen vermeiden, Komponententests für eine Schnittstelle zu schreiben, aus dem einfachen Grund, dass eine Schnittstelle, wie sehr Sie es auch möchten, keine Funktionalität definiert . Es belastet seine Implementierer mit syntaktischen Anforderungen, aber das war's.

Umgekehrt sollen Unit-Tests sicherstellen, dass die von Ihnen erwartete Funktionalität in einem bestimmten Codepfad vorhanden ist.

Abgesehen davon gibt es Situationen, in denen diese Art von Test sinnvoll sein könnte. Angenommen, Sie möchten, dass diese Tests sicherstellen, dass von Ihnen geschriebene Klassen (die eine bestimmte Schnittstelle gemeinsam nutzen) tatsächlich dieselbe Funktionalität haben, dann würde ich Ihre erste Option vorziehen. Dies macht es für die implementierenden Unterklassen am einfachsten, sich in den Testprozess einzubringen. Ich glaube auch nicht, dass dein "Betrug" wirklich wahr ist. Es gibt keinen Grund, warum Sie nicht können, dass die tatsächlich getesteten Klassen ihre eigenen Mocks bereitstellen (obwohl ich denke, wenn Sie wirklich unterschiedliche Mocks benötigen, deutet dies darauf hin, dass Ihre Schnittstellentests sowieso nicht einheitlich sind.)

dlev
quelle
27
Eine Schnittstelle definiert keine Funktionalität, aber sie definiert die API, an die sich ihre Implementierungen halten müssen. Darauf sollte sich ein Test konzentrieren. Warum sollte man dann keinen Test schreiben, um das auszudrücken? Insbesondere wenn Sie den Polymorphismus mit mehreren Implementierungen einer Schnittstelle gut nutzen, ist diese Art von Test äußerst wertvoll.
Ryan Stewart
2
Ich stimme dlev zu - ich sehe keinen Sinn darin, eine Schnittstelle zu testen. Der Compiler teilt Ihnen mit, ob Ihre konkrete Implementierung die Schnittstelle nicht implementiert. Ich sehe überhaupt keinen Wert darin. Unit Tests sind für konkrete Klassen.
Duffymo
5
@ Ryan - ein Schnittstellenvertrag ist selten "nur eine Konvention". Ein Vertrag kommuniziert feste Erwartungen der Empfänger der Schnittstelle. Vertragsverletzungen können vom Compiler zugelassen werden, führen jedoch häufig zur Laufzeit zu unerwartetem Verhalten. Eine einzige Definition dieses Vertrags in Unit-Tests ist besser als mehrere.
Andy Thomas
1
Für Leute, die nicht damit einverstanden sind, Komponententests für eine Schnittstelle zu schreiben - wenn es für LinkedList mehrere Implementierungen wie Singly, Doubly, Circular usw. gibt, warum sollte ich die üblichen Komponententests neu schreiben, ist es nicht bequemer, einen parametrisierten Test durchzuführen? @duffymo bist du der gleiche Duffymo aus Sonnenforen?
ChrisOdney
5
Ich würde über die Schnittstelle nur Funktionen testen, die Teil des Vertrags sind, z. B.: Wenn das Element zum ersten Mal zur Liste hinzugefügt wird, sollte die Größe 1 werden, unabhängig davon, welche Implementierung verwendet wird, sollte diese Invariante gelten. Für die Implementierungen, die nur für diese Implementierung spezifische Funktionen testen, sind jedoch möglicherweise zusätzliche Tests erforderlich.
siebzehn
1

mit java 8 mache ich das

public interface MyInterfaceTest {
   public MyInterface createInstance();

   @Test
   default void testMyMethod_True() {
       MyInterface instance = createInstance();
       assertTrue(instance.myMethod(true));
   }

   @Test
   default void testMyMethod_False() {
       MyInterface instance = createInstance();
       assertFalse(instance.myMethod(false));
   }
}

public class MyClass1Test implements MyInterfaceTest {
    public MyInterface createInstance() {
        return new MyClass1();
    }
}

public class MyClass2Test implements MyInterfaceTest {
   public MyInterface createInstance() {
       return new MyClass2();
   }

   @Disabled
   @Override
   @Test
   public void testMyMethod_True() {
       MyInterfaceTest.super.testMyMethod_True();
   };
}
Xavier Gouraud
quelle
Das Schlüsselwort "abstrakt" ist unnötig.
Ubuntix
Dieser Ansatz wird ausführlicher im JUnit 5-Benutzerhandbuch im Abschnitt Testschnittstellen und Standardmethoden dokumentiert .
Geert-Jan Hut