web-dev-qa-db-fra.com

Écriture d'un test unitaire pour plusieurs implémentations d'une interface

J'ai une interface List dont les implémentations incluent Singly Linked List, Doubly, Circular, etc. Les tests unitaires que j'ai écrits pour Singly devraient être utiles à Doubly, Circular et toute autre nouvelle implémentation de l'interface. Ainsi, au lieu de répéter les tests unitaires pour chaque implémentation, JUnit propose-t-il quelque chose de intégré qui me permettrait d’avoir un test JUnit et de le lancer contre différentes implémentations?

En utilisant des tests paramétrés JUnit, je peux fournir différentes implémentations telles que Singly, doublement, circulaire, etc., mais pour chaque implémentation, le même objet est utilisé pour exécuter tous les tests de la classe.

48
ChrisOdney

Avec JUnit 4.0+, vous pouvez utiliser tests paramétrés :

  • Ajouter l'annotation @RunWith(value = Parameterized.class) à votre appareil de test
  • Créez une méthode public static renvoyant Collection, annotez-la avec @Parameters et insérez SinglyLinkedList.class, DoublyLinkedList.class, CircularList.class, etc. dans cette collection.
  • Ajoutez un constructeur à votre appareil de test qui prend Class: public MyListTest(Class cl) et stockez la Class dans une variable d'instance listClass
  • Dans la méthode setUp ou @Before, utilisez List testList = (List)listClass.newInstance();

Une fois la configuration ci-dessus en place, le programme paramétré créera une nouvelle instance de votre appareil de test MyListTest pour chaque sous-classe fournie dans la méthode @Parameters, vous permettant ainsi d'appliquer la même logique de test pour chaque sous-classe à tester.

34
dasblinkenlight

J'éviterais probablement les tests paramétrés de JUnit (qui à mon humble avis sont assez mal implémentés) et crée simplement une classe de test abstraite List qui pourrait être héritée par les implémentations de tests:

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(){ /* ... */ }

}

Les différentes implémentations obtiennent ensuite leurs propres classes concrètes:

class SinglyLinkedListTest extends ListTestBase<SinglyLinkedList> {

    @Override
    protected SinglyLinkedList createInstance(){ 
        return new SinglyLinkedList(); 
    }

}

class DoublyLinkedListTest extends ListTestBase<DoublyLinkedList> {

    @Override
    protected DoublyLinkedList createInstance(){ 
        return new DoublyLinkedList(); 
    }

}

La bonne chose à propos de le faire de cette façon (au lieu de créer une classe de test qui teste toutes les implémentations) est que, si vous souhaitez tester certains cas spécifiques, vous pouvez simplement ajouter plusieurs tests à la sous-classe de test spécifique. .

51
gustafc

Je sais que cela est vieux, mais j’ai appris à le faire dans une variante légèrement différente qui fonctionne bien, dans laquelle vous pouvez appliquer le @Parameter à un membre de champ pour injecter les valeurs. 

C'est juste un peu plus propre à mon avis.

@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

Basé sur les réponses de @dasblinkenlight et this , j'ai proposé une implémentation que je souhaiterais partager avec mon cas d'utilisation.

J'utilise ServiceProviderPattern ( API de différence et SPI ) pour les classes qui implémentent l'interface IImporterService. Si une nouvelle implémentation de l'interface est développée, seul un fichier de configuration dans META-INF/services/ doit être modifié pour enregistrer l'implémentation.

Le fichier dans META-INF/services/ est nommé d'après le nom de classe complet de l'interface de service (IImporterService), par exemple. 

de.myapp.importer.IImporterService

Ce fichier contient une liste des casses qui implémentent IImporterService, par exemple.

de.myapp.importer.impl.OfficeOpenXMLImporter

La classe d'usine ImporterFactory fournit aux clients des implémentations concrètes de l'interface.


La ImporterFactory renvoie une liste de toutes les implémentations de l'interface, enregistrées via le ServiceProviderPattern . La méthode setUp() garantit qu'une nouvelle instance est utilisée pour chaque scénario de test.

@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() {
    }
}

La méthode ImporterFactory.INSTANCE.getImplementations() se présente comme suit:

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

Vous pouvez réellement créer une méthode d'assistance dans votre classe de test qui configure votre test List en tant qu'instance d'une de vos implémentations dépendant d'un argument . En combinaison avec this , vous devriez pouvoir obtenir le comportement tu veux.

0
LionC

En développant la première réponse, les aspects Parameter de JUnit4 fonctionnent très bien. Voici le code que j'ai utilisé dans un projet testant des filtres. La classe est créée à l'aide d'une fonction fabrique (getPluginIO) et la fonction getPluginsNamed obtient toutes les classes PluginInfo avec le nom utilisant SezPoz et des annotations afin de permettre la détection automatique des nouvelles classes. 

@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

Notez qu'il est important (personnellement, je ne sais pas pourquoi cela fonctionne de cette façon) de faire en sorte que la collection soit un ensemble de tableaux du paramètre réel transmis au constructeur, dans ce cas une classe appelée PluginInfo. La fonction statique wrapCollection effectue cette tâche.

/**
 * 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