it-swarm.com.de

Schreiben eines einzelnen Komponententests für mehrere Implementierungen einer Schnittstelle

Ich habe eine Schnittstelle List, zu deren Implementierungen Singly Linked List, Doubly, Circular usw. gehören. Die Unit-Tests, die ich für Singly geschrieben habe, sollten für die meisten von Doubly sowie Circular und jede andere neue Implementierung der Schnittstelle gut sein. Bietet JUnit also nicht die Unit-Tests für jede Implementierung zu wiederholen, sondern ein integriertes System, mit dem ich einen JUnit-Test ausführen könnte, um ihn für verschiedene Implementierungen auszuführen?

Mit parametrisierten JUnit-Tests kann ich verschiedene Implementierungen bereitstellen, z. B. "Singly", "Double", "Circular" usw. Für jede Implementierung wird jedoch das gleiche Objekt verwendet, um alle Tests in der Klasse auszuführen.

48
ChrisOdney

Mit JUnit 4.0+ können Sie parametrisierte Tests verwenden:

  • Fügen Sie Ihrem Testgerät @RunWith(value = Parameterized.class) Annotation hinzu
  • Erstellen Sie eine public static-Methode, die Collection zurückgibt, kommentieren Sie sie mit @Parameters und fügen Sie SinglyLinkedList.class, DoublyLinkedList.class, CircularList.class usw. in diese Auflistung ein
  • Fügen Sie Ihrem Test-Fixture einen Konstruktor hinzu, der Class: public MyListTest(Class cl) verwendet, und speichern Sie die Class in einer Instanzvariablen listClass.
  • Verwenden Sie in der setUp-Methode oder @BeforeList testList = (List)listClass.newInstance();

Mit dem obigen Setup erstellt der parametrisierte Läufer eine neue Instanz Ihres Test-Fixtures MyListTest für jede Unterklasse, die Sie in der @Parameters-Methode angeben, und lässt Sie für jede zu testende Unterklasse die gleiche Testlogik anwenden.

34
dasblinkenlight

Ich würde wahrscheinlich die parametrisierten Tests von JUnit (die IMHO ziemlich unhandlich implementiert sind) vermeiden und einfach eine abstrakte List-Testklasse erstellen, die von Testimplementierungen 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 erhalten dann ihre eigenen konkreten 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(); 
    }

}

Das Schöne daran, dies auf diese Weise zu tun (anstatt eine Testklasse zu erstellen, die alle Implementierungen testet) ist, dass es einige spezielle Eckfälle gibt, die Sie mit einer Implementierung testen möchten, Sie können einfach weitere Tests zur jeweiligen Testunterklasse hinzufügen .

51
gustafc

Ich weiß, das ist alt, aber ich habe gelernt, dies in einer etwas anderen Variante zu tun, die gut funktioniert, wobei Sie den @Parameter auf ein Feldmitglied anwenden können, um die Werte einzufügen. 

Meiner Meinung nach ist es nur ein bisschen sauberer.

@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();
    }
3
mcnichol

Basierend auf der Antwort auf @dasblinkenlight und this anwser habe ich eine Implementierung für meinen Use Case entwickelt, die ich gerne teilen möchte.

Ich benutze die ServiceProviderPattern ( difference API) und SPI ) für Klassen, die das Interface 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 nach dem vollständig qualifizierten Klassennamen der Dienstschnittstelle (IImporterService) benannt, z. 

de.myapp.importer.IImporterService

Diese Datei enthält eine Liste von Kassetten, die IImporterService implementieren, z.

de.myapp.importer.impl.OfficeOpenXMLImporter

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


Die Variable ImporterFactory gibt eine Liste aller Implementierungen der Schnittstelle zurück, die über ServiceProviderPattern registriert wurden. Die setUp()-Methode 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 folgendermaßen aus:

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

Sie könnten tatsächlich eine Hilfsmethode in Ihrer Testklasse erstellen, die Ihren Test List so konfiguriert, dass er eine Instanz einer Ihrer Implementierungen ist, die von einem Argument abhängig ist ..__ In Kombination mit this sollten Sie das Verhalten abrufen können Sie wollen.

0
LionC

Die Parameteraspekte von JUnit4 arbeiten sehr gut mit der ersten Antwort. Hier ist der Code, den ich in einem Projekttestfilter verwendet habe. Die Klasse wird mit einer Factory-Funktion (getPluginIO) erstellt. Die Funktion getPluginsNamed ruft alle PluginInfo-Klassen mit dem Namen SezPoz und Anmerkungen ab, damit neue Klassen automatisch erkannt werden können. 

@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

Beachten Sie, dass es wichtig ist (ich persönlich habe keine Ahnung, warum es so funktioniert), dass die Sammlung als Sammlung von Arrays des Aktualparameters in den Konstruktor eingegeben wird, in diesem Fall eine Klasse namens PluginInfo. 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;
}
0
kmader