validacion unitarias unitaria tutorial software pruebas prueba español ejercicios ejemplos ejemplo configuracion con java unit-testing junit junit4 junit-runner

java - unitarias - Escribir una prueba de unidad única para implementaciones múltiples de una interfaz



pruebas unitarias (6)

Tengo una List interfaces cuyas implementaciones incluyen Singly Linked List, Double, Circular, etc. Las pruebas unitarias que escribí para Singly deberían ser buenas para la mayor parte de Double, así como Circular y cualquier otra implementación nueva de la interfaz. Entonces, en lugar de repetir las pruebas unitarias para cada implementación, ¿ofrece JUnit algo incorporado que me permita tener una prueba JUnit y ejecutarla contra diferentes implementaciones?

Utilizando las pruebas parametrizadas JUnit, puedo suministrar diferentes implementaciones como Singly, doblemente, circular, etc., pero para cada implementación se usa el mismo objeto para ejecutar todas las pruebas en la clase.


Ampliando en la primera respuesta, los aspectos de parámetros de JUnit4 funcionan muy bien. Aquí está el código real que utilicé en los filtros de prueba de un proyecto. La clase se crea usando una función de fábrica ( getPluginIO ) y la función getPluginsNamed obtiene todas las clases de PluginInfo con el nombre usando SezPoz y anotaciones para permitir la detección automática de nuevas clases.

@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

Tenga en cuenta que es importante (personalmente no tengo ni idea de por qué funciona de esta manera) tener la colección como una colección de matrices del parámetro real alimentado al constructor, en este caso una clase llamada PluginInfo. La función estática wrapCollection realiza esta tarea.

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


Basándome en el mensaje de @dasblinkenlight y en this artículo, se me ocurrió una implementación para mi caso de uso que me gustaría compartir.

Uso ServiceProviderPattern ( diferencia API y SPI ) para las clases que implementan la interfaz IImporterService . Si se desarrolla una nueva implementación de la interfaz, solo se debe modificar un archivo de configuración en META-INF / services / para registrar la implementación.

El archivo en META-INF / services / lleva el nombre del nombre de clase completo de la interfaz de servicio ( IImporterService ), por ejemplo

de.myapp.importer.IImporterService

Este archivo contiene una lista de cassas que implementan IImporterService , por ejemplo

de.myapp.importer.impl.OfficeOpenXMLImporter

La clase de fábrica ImporterFactory proporciona a los clientes implementaciones concretas de la interfaz.

ImporterFactory devuelve una lista de todas las implementaciones de la interfaz, registrada a través de ServiceProviderPattern . El método setUp() asegura que se use una nueva instancia para cada caso de prueba.

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

El método ImporterFactory.INSTANCE.getImplementations() tiene el siguiente aspecto:

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


Con JUnit 4.0+ puedes usar pruebas parametrizadas :

  • Agregue la @RunWith(value = Parameterized.class) a su accesorio de prueba
  • Cree un método public static que devuelva Collection , @Parameters con @Parameters y coloque SinglyLinkedList.class , DoublyLinkedList.class , CircularList.class , etc. en esa colección
  • Agregue un constructor a su accesorio de prueba que tome Class : public MyListTest(Class cl) , y almacene la Class en una variable de instancia listClass
  • En el método setUp o @Before , use List testList = (List)listClass.newInstance();

Con la configuración anterior en su lugar, el corredor parametrizado creará una nueva instancia de su dispositivo de prueba MyListTest para cada subclase que proporcione en el método @Parameters , permitiéndole ejercitar la misma lógica de prueba para cada subclase que deba probar.


En realidad, podría crear un método auxiliar en su clase de prueba que configure su List prueba para que sea una instancia de una de sus implementaciones que dependa de un argumento. En combinación con this , debería poder obtener el comportamiento que desea.


Probablemente evitaría las pruebas parametrizadas de JUnit (que en mi humilde opinión son implementadas de manera bastante torpe), y simplemente crearé una clase de prueba de List abstracta que podría heredarse mediante implementaciones de pruebas:

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

Las diferentes implementaciones obtienen sus propias clases concretas:

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

Lo bueno de hacerlo de esta manera (en lugar de hacer una clase de prueba que pruebe todas las implementaciones) es que si hay algunos casos de esquina específicos que le gustaría probar con una implementación, puede agregar más pruebas a la subclase de prueba específica .


Sé que esto es viejo, pero aprendí a hacer esto en una variación ligeramente diferente que funciona muy bien en donde puedes aplicar el @Parameter a un miembro del campo para inyectar los valores.

Es un poco más limpio en mi opinión.

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