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?
Antworten:
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()} ); } }
quelle
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.
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.
Verwenden Sie Testsuiten .
quelle
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 }
quelle
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
@Parameterized
Tests 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.
quelle
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.)
quelle
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(); }; }
quelle