2013-04-26 9 views
40

Ich habe eine Schnittstelle List, deren Implementierungen Singly Linked List, Doubly, Circular usw. enthalten. Die Unit-Tests, die ich für Singly geschrieben habe, sollten gut für die meisten von Doubly sowie Circular und jede andere neue Implementierung der Schnittstelle. Anstatt also die Komponententests für jede Implementierung zu wiederholen, bietet JUnit etwas eingebautes, das mir einen JUnit-Test erlauben und gegen verschiedene Implementierungen laufen lassen könnte?Schreiben eines einzelnen Einheitentests für mehrere Implementierungen einer Schnittstelle

Mit JUnit parametrisierten Tests kann ich verschiedene Implementierungen wie Singly, doppelt, zirkuläre usw. liefern, aber für jede Implementierung wird das gleiche Objekt verwendet, um alle Tests in der Klasse auszuführen.

+0

was meinst du "das gleiche Objekt wird verwendet, um alle Tests auszuführen"? –

+0

Als ehemaliger Junit-Süchtiger möchte ich nur sagen, dass man sich groovy/spock anschauen sollte. Spock ist cool und groovy gibt dir einige Fähigkeiten, die du mit Junit einfach nicht kannst. Eine meiner Lieblingssachen ist der Zugriff auf private Datenmitglieder, so dass Sie nicht nur etwas offenlegen müssen, um einen korrekten Komponententest zu erstellen. – Thom

Antwort

31

Mit JUnit 4.0+ Sie parameterized tests verwenden können:

  • hinzufügen @RunWith(value = Parameterized.class) Anmerkung zu Ihrem Prüfadapter
  • erstellen public static Methode Rückkehr Collection, mit Anmerkungen versehen es mit @Parameters und legte SinglyLinkedList.class, DoublyLinkedList.class, CircularList.class, etc

      In diese Sammlung
    • Fügen Sie Ihrem Testgerät einen Konstruktor hinzu, der Class: public MyListTest(Class cl) verwendet, und speichern Sie die Class in einer Instanzvariablen listClass
    • Im setUp Methode oder @Before, die Sie List testList = (List)listClass.newInstance();

    Mit der obigen Setup verwenden erfolgt ist, wird die parametrisierte Läufer eine neue Instanz der Testvorrichtung machen MyListTest für jede Unterklasse bieten In der Methode @Parameters können Sie die gleiche Testlogik für jede zu testende Unterklasse verwenden.

  • +0

    Verdammt! Warum habe ich nicht darüber nachgedacht? Vielen Dank. – ChrisOdney

    +0

    Wie wäre es mit List testList = (Liste) listClass.newInstance(); in der setUp-Methode statt jeder Testmethode? – ChrisOdney

    +0

    @ChrisOdney Ja, das würde auch funktionieren. Sie können auch eine 'makeList()' -Methode erstellen, falls einige Ihrer Testmethoden mehrere Instanzen der Listenklasse erstellen müssen. – dasblinkenlight

    42

    Ich würde wahrscheinlich JUnit parametrisierte Tests (die IMHO sind ziemlich plump implementiert), und so stellen eine abstrakte List Testklasse vermeiden, die durch Tests Implementierungen vererbt werden könnte:

    public abstract class ListTestBase<T extends List> { 
    
        private T instance; 
    
        protected abstract T createInstance(); 
    
        @Before 
        public void setUp() { 
         instance = createInstance(); 
        } 
    
        @Test 
        public void testOneThing(){ /* ... */ } 
    
        @Test 
        public void testAnotherThing(){ /* ... */ } 
    
    } 
    

    Die verschiedenen Implementierungen dann bekommen ihre eigene konkrete Klassen:

    class SinglyLinkedListTest extends ListTestBase<SinglyLinkedList> { 
    
        @Override 
        protected SinglyLinkedList createInstance(){ 
         return new SinglyLinkedList(); 
        } 
    
    } 
    
    class DoublyLinkedListTest extends ListTestBase<DoublyLinkedList> { 
    
        @Override 
        protected DoublyLinkedList createInstance(){ 
         return new DoublyLinkedList(); 
        } 
    
    } 
    

    Die nette Sache es auf diese Weise zu tun (statt einer Testklasse zu machen, die alle Implementierungen Tests) ist, dass wenn es einige spezielle Ecke Fälle würden Sie gerne Testen Sie mit einer Implementierung, Sie können einfach weitere Tests zu der spezifischen Testunterklasse hinzufügen.

    +2

    Danke für die Antwort, es ist eleganter als Junit Paramterized Tests und ich werde es wahrscheinlich verwenden. Aber ich muss mich an die Antwort von dasblinkenlight halten, da ich mit den parametrisierten Tests von Junit nach einem Ausweg suchte. – ChrisOdney

    +1

    Sie könnten es auf diese Weise tun, oder parametrisierte Tests verwenden und annehmen. Wenn es eine Testmethode gibt, die nur für eine (oder mehrere) bestimmte Klasse gilt, dann würde man zu Beginn des Tests davon ausgehen, dass dieser Test für andere Klassen ignoriert würde. –

    +0

    Ich denke das ist eine Basis für eine großartige Lösung. Es ist wichtig, alle von einem Objekt implementierten Schnittstellenmethoden testen zu können. Stellen Sie sich jedoch eine RepositoryGateway-Schnittstelle mit Methoden wie saveUser (user) und findUserById (id) vor, die für verschiedene Datenbanken implementiert werden sollten. Für findUserById (id) muss das testmethodspezifische Setup die spezifische Datenbank mit dem zu suchenden Benutzer füllen. Für saveUser (Benutzer) sollte der Assert-Teil Daten von der spezifischen Datenbank abrufen. Könnte dies durch Hinzufügen eines Hooks (wie prepareFindUser) in der Testmethode gelöst werden, implementiert durch die spezifische Testklasse? –

    0

    Sie könnten tatsächlich eine Hilfsmethode in Ihrer Testklasse erstellen, die Ihren Test List als eine Instanz einer Ihrer Implementierungen abhängig von einem Argument einrichtet. In Kombination mit this sollten Sie in der Lage sein, das gewünschte Verhalten zu erhalten.

    0

    Erweiterung der ersten Antwort, die Parameteraspekte von JUnit4 funktionieren sehr gut. Hier ist der eigentliche Code, den ich in einem Projekt verwendet habe, um Filter zu testen. Die Klasse wird mit einer Factory-Funktion (getPluginIO) erstellt und die Funktion getPluginsNamed ruft alle PluginInfo-Klassen mit dem Namen mithilfe von SezPoz und Anmerkungen ab, damit neue Klassen automatisch erkannt werden.

    @RunWith(value=Parameterized.class) 
    public class FilterTests { 
    @Parameters 
    public static Collection<PluginInfo[]> getPlugins() { 
        List<PluginInfo> possibleClasses=PluginManager.getPluginsNamed("Filter"); 
        return wrapCollection(possibleClasses); 
    } 
    final protected PluginInfo pluginId; 
    final IOPlugin CFilter; 
    public FilterTests(final PluginInfo pluginToUse) { 
        System.out.println("Using Plugin:"+pluginToUse); 
        pluginId=pluginToUse; // save plugin settings 
        CFilter=PluginManager.getPluginIO(pluginId); // create an instance using the factory 
    } 
    //.... the tests to run 
    

    Hinweis es wichtig ist, (Ich persönlich habe keine Ahnung, warum es so funktioniert), um die Sammlung als eine Sammlung von Arrays des aktuellen Parameters an den Konstruktor zugeführt haben, in diesem Fall eine Klasse namens Plugin. Die statische Funktion wrapCollection führt diese Aufgabe aus.

    /** 
    * Wrap a collection into a collection of arrays which is useful for parameterization in junit testing 
    * @param inCollection input collection 
    * @return wrapped collection 
    */ 
    public static <T> Collection<T[]> wrapCollection(Collection<T> inCollection) { 
        final List<T[]> out=new ArrayList<T[]>(); 
        for(T curObj : inCollection) { 
         T[] arr = (T[])new Object[1]; 
         arr[0]=curObj; 
         out.add(arr); 
        } 
        return out; 
    } 
    
    1

    Basierend auf den anwser von @dasblinkenlight und this anwser ich mit einer Implementierung für meinen Anwendungsfall kam, Ich mag würde teilen.

    Ich verwende die ServiceProviderPattern (difference API and SPI) für Klassen, die die Schnittstelle IImporterService implementieren. Wenn eine neue Implementierung der Schnittstelle entwickelt wird, muss nur eine Konfigurationsdatei in META-INF/services/ geändert werden, um die Implementierung zu registrieren.

    Die Datei in META-INF/services/ ist benannt nach dem voll qualifizierten Klassennamen der Service-Schnittstelle (IImporterService), z.B.

    de.myapp.importer.IImporterService

    Diese Datei enthält eine Liste von casses die IImporterService, zum Beispiel implementieren

    de.myapp.importer.impl.OfficeOpenXMLImporter

    Die Factory-Klasse ImporterFactory bietet seinen Kunden konkrete Implementierungen der Schnittstelle.


    Die ImporterFactory gibt eine Liste aller Implementierungen der Schnittstelle, über die ServiceProviderPattern registriert. Die Methode setUp() stellt sicher, dass für jeden Testfall eine neue Instanz verwendet wird.

    @RunWith(Parameterized.class) 
    public class IImporterServiceTest { 
        public IImporterService service; 
    
        public IImporterServiceTest(IImporterService service) { 
         this.service = service; 
        } 
    
        @Parameters 
        public static List<IImporterService> instancesToTest() { 
         return ImporterFactory.INSTANCE.getImplementations(); 
        } 
    
        @Before 
        public void setUp() throws Exception { 
         this.service = this.service.getClass().newInstance(); 
        } 
    
        @Test 
        public void testRead() { 
        } 
    } 
    

    Die ImporterFactory.INSTANCE.getImplementations() Methode sieht wie folgt aus:

    public List<IImporterService> getImplementations() { 
        return (List<IImporterService>) GenericServiceLoader.INSTANCE.locateAll(IImporterService.class); 
    } 
    
    3

    Ich weiß, das ist alt, aber ich lernte dies in einer etwas anderen Variante zu tun, was gut funktioniert, wobei Sie die @Parameter auf eine anwenden können Feldmitglied, um die Werte zu injizieren.

    Es ist nur ein bisschen sauberer meiner Meinung nach.

    @RunWith(Parameterized.class) 
    public class MyTest{ 
    
        private ThingToTest subject; 
    
        @Parameter 
        public Class clazz; 
    
        @Parameters(name = "{index}: Impl Class: {0}") 
        public static Collection classes(){ 
         List<Object[]> implementations = new ArrayList<>(); 
         implementations.add(new Object[]{ImplementationOne.class}); 
         implementations.add(new Object[]{ImplementationTwo.class}); 
    
         return implementations; 
        } 
    
        @Before 
        public void setUp() throws Exception { 
         subject = (ThingToTest) clazz.getConstructor().newInstance(); 
        } 
    
    Verwandte Themen