Rédiger un test unitaire pour plusieurs implémentations d’une interface

J’ai une List interfaces dont les implémentations incluent Singly Linked List, Doubly, Circular etc. Les tests unitaires que j’ai écrits pour Singly devraient faire du bien pour la plupart de Doubly ainsi que pour Circular et toute autre nouvelle implémentation de l’interface. Donc, au lieu de répéter les tests unitaires pour chaque implémentation, JUnit offre-t-il quelque chose qui pourrait me permettre d’avoir un test JUnit et de l’exécuter avec différentes implémentations?

En utilisant les tests paramétrés JUnit, je peux fournir différentes implémentations comme Singly, Doubly, Circular etc., mais pour chaque implémentation, le même object est utilisé pour exécuter tous les tests de la classe.

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

  • Ajouter l’ @RunWith(value = Parameterized.class) à votre @RunWith(value = Parameterized.class) test
  • Créez une méthode public static renvoyant Collection , @Parameters avec @Parameters et insérez SinglyLinkedList.class , DoublyLinkedList.class , CircularList.class , etc. dans cette collection.
  • Ajoutez un constructeur à votre dispositif de test qui prend la 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();

Avec la configuration ci-dessus en place, le runner paramétré créera une nouvelle instance de votre assembly de test MyListTest pour chaque sous-classe que vous fournissez dans la méthode @Parameters , vous permettant d’exercer la même logique de test pour chaque sous-classe à tester.

J’éviterais probablement les tests paramétrés de JUnit (dont l’implémentation est plutôt maladroite), et je ferais juste une classe de test de List abstraite qui pourrait être héritée par les implémentations de tests:

 public abstract class ListTestBase { 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 alors leurs propres classes concrètes:

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

La bonne chose à faire de cette façon (au lieu de créer une seule classe de test qui teste toutes les implémentations) est que si vous souhaitez tester quelques cas particuliers avec une seule implémentation, vous pouvez simplement append d’autres tests à la sous-classe de test spécifique. .

Je sais que c’est ancien, mais j’ai appris à le faire dans une variante légèrement différente, qui fonctionne parfaitement, dans laquelle vous pouvez appliquer le @Parameter à un membre du 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 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(); } 

Sur la base de la réponse de @dasblinkenlight et de cette réponse, je suis venu avec une implémentation pour mon cas d’utilisation que je voudrais partager.

J’utilise le 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 de 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.


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

 @RunWith(Parameterized.class) public class IImporterServiceTest { public IImporterService service; public IImporterServiceTest(IImporterService service) { this.service = service; } @Parameters public static List 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() ressemble à ceci:

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

Vous pourriez en fait créer une méthode d’assistance dans votre classe de test qui configure votre List test pour qu’elle soit une instance de l’une de vos implémentations dépendant d’un argument. En combinaison avec cela, vous devriez être capable d’obtenir le comportement que vous souhaitez.

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 pour tester les filtres. La classe est créée en utilisant une fonction usine ( getPluginIO ) et la fonction getPluginsNamed obtient toutes les classes PluginInfo avec le nom en utilisant SezPoz et des annotations pour permettre la détection automatique de nouvelles classes.

 @RunWith(value=Parameterized.class) public class FilterTests { @Parameters public static Collection getPlugins() { List 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) d’avoir la collection en tant que collection de tableaux du paramètre réel fourni au constructeur, en l’occurrence 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  Collection wrapCollection(Collection inCollection) { final List out=new ArrayList(); for(T curObj : inCollection) { T[] arr = (T[])new Object[1]; arr[0]=curObj; out.add(arr); } return out; }