it-swarm.com.de

Wie schreibe ich Junit-Tests für Schnittstellen?

Wie lassen sich Junit-Tests für Schnittstellen am besten schreiben, damit sie für die konkreten Implementierungsklassen verwendet werden können?

z.B. 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:

  • feinkörnigkeit für jeden Test, einschließlich Abhängigkeiten und Scheinobjekten

Con:

  • Für jede implementierende Testklasse müssen zusätzliche Testmethoden geschrieben werden

Welche Möglichkeit würdest du bevorzugen oder welchen anderen Weg nutzt du?

68
Xeno Lupus

Im Gegensatz zu der vielbeachteten 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 über ihre Schnittstelle, ist das wichtigste zu testende Element. Abgesehen davon würde ich keinen der von Ihnen genannten Ansätze verwenden, sondern 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()}
        );
    }
}
72
Ryan Stewart

Ich bin mit @dlev überhaupt nicht einverstanden. Sehr oft ist es eine sehr gute Übung, 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.

Also kenne ich 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 .

18
AlexR

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

Sie möchten wahrscheinlich parametrisierte Tests verwenden. So würde es mit TestNG aussehen, mit JUnit ist es etwas komplizierter (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
}
14
Cedric Beust

Späte Hinzufügung zum Thema, Austausch neuerer Lösungserkenntnisse

Ich bin auch auf der Suche nach einer geeigneten und effizienten Methode zum Testen (basierend auf JUnit) der Korrektheit mehrerer Implementierungen einiger Schnittstellen und abstrakter Klassen. Leider entsprechen weder die Tests von JUnit @Parameterized Noch das entsprechende Konzept von TestNG meinen Anforderungen, da ich a priori die Liste der Implementierungen dieser Schnittstellen-/abstrakten Klassen, die möglicherweise existieren, nicht kenne. Das heißt, dass möglicherweise neue Implementierungen entwickelt werden und Tester möglicherweise nicht auf alle vorhandenen Implementierungen zugreifen können. Es ist daher nicht effizient, Testklassen die Liste der Implementierungsklassen angeben zu lassen.

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 erlaubt es die Definition von "Vertragstests" durch die Annotation @Contract(InterfaceClass.class) für Vertragstestklassen. Dann würde ein Implementierer eine implementierungsspezifische Testklasse mit den Anmerkungen @RunWith(ContractSuite.class) und @ContractImpl(value = ImplementationClass.class) erstellen. Die Engine wendet automatisch alle für ImplementationClass geltenden Vertragstests an, indem sie nach allen Vertragstests sucht, die für eine Schnittstelle oder eine 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, jedoch viel spezifischeres Bedürfnis, das eine Klassenkonformität speziell für Object.equals- und Object.hashcode-Verträge geltend macht.

In ähnlicher Weise demonstrieren https://bitbucket.org/chas678/testhelpers/src eine Strategie zur Validierung einiger grundlegender Java Verträge, einschließlich Object.equals, Object.hashcode, Comparable.compare , Serialisierbar. In diesem Projekt werden einfache Teststrukturen verwendet, die meines Erachtens problemlos reproduziert werden können, um den jeweiligen Anforderungen zu entsprechen.

Nun, das ist es für jetzt; Ich werde diesen Beitrag mit weiteren nützlichen Informationen, die ich finden kann, auf dem Laufenden halten.

10
jwatkins

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

Unit-Tests hingegen sollen 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 einzuschleusen. Ich glaube auch nicht, dass dein "Betrug" wirklich wahr ist. Es gibt keinen Grund, warum Sie die getesteten Klassen nicht dazu bringen können, ihre eigenen Mocks bereitzustellen.

5
dlev

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();
   };
}
1
Xavier Gouraud