2013-04-26 17 views
40

Ho un'interfaccia List le cui implementazioni includono Elenco collegato singolarmente, Doubly, Circular ecc. I test di unità che ho scritto per Singly dovrebbero fare del bene per la maggior parte di Doubly nonché Circular e qualsiasi altra nuova implementazione dell'interfaccia. Quindi, invece di ripetere i test unitari per ogni implementazione, JUnit offre qualcosa di integrato che mi permetterebbe di avere un test JUnit ed eseguirlo contro diverse implementazioni?Scrittura di un test unitario per implementazioni multiple di un'interfaccia

Utilizzo di test parametrici JUnit Sono in grado di fornire diverse implementazioni come Singly, doublely, circular ecc. Ma per ogni implementazione lo stesso oggetto viene utilizzato per eseguire tutti i test della classe.

+0

cosa intendi "lo stesso oggetto viene utilizzato per eseguire tutti i test"? –

+0

Come ex dipendente da una junit, vorrei solo dire che dovresti dare un'occhiata a groovy/spock. Spock è cool e groovy ti dà alcune abilità che non puoi fare con junit. Uno dei miei aspetti preferiti è l'accesso ai membri di dati privati, quindi non devi esporre qualcosa solo per creare un test unitario adeguato. – Thom

risposta

31

Con JUnit 4.0+ è possibile utilizzare parameterized tests:

  • Aggiungi @RunWith(value = Parameterized.class) annotazione al vostro dispositivo di prova
  • Creare un metodo public static ritorno Collection, annotare con @Parameters, e mettere SinglyLinkedList.class, DoublyLinkedList.class, CircularList.class, ecc in quella collezione
  • Aggiungere un costruttore al dispositivo di prova che prende Class: public MyListTest(Class cl) e memorizzare Class in un'istanza variabile listClass
  • Nel metodo setUp o @Before, utilizzare List testList = (List)listClass.newInstance();

Con l'impostazione sopra al suo posto, il corridore parametrizzato farà una nuova istanza del dispositivo di prova MyListTest per ogni sottoclasse che fornite nel metodo @Parameters, che consente di esercitare la stessa logica di test per ogni sottoclasse da testare.

+0

Dannazione! Perché non ci ho pensato. Grazie. – ChrisOdney

+0

Che ne dici di fare List testList = (List) listClass.newInstance(); nel metodo setUp invece di ogni metodo di prova? – ChrisOdney

+0

@ChrisOdney Sì, funzionerebbe altrettanto bene. Potresti anche creare un metodo 'makeList()' di aiuto nel caso in cui alcuni dei tuoi metodi di test debbano creare diverse istanze della classe list. – dasblinkenlight

42

avrei probabilmente evitare i test con parametri di JUnit (che secondo me sono piuttosto maldestramente attuato), e solo fare un abstract List classe di test che potrebbe essere ereditato da prove implementazioni:

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

} 

Le diverse implementazioni quindi ottenere la loro proprie classi concrete:

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 cosa bella di fare in questo modo (invece di fare una classe di test che verifica tutte le implementazioni) è che se ci sono alcuni casi angolo specifici che desideri prova con una implementazione, puoi semplicemente aggiungere più test alla sottoclasse test specifica.

+2

Grazie per la risposta, è più elegante dei test Paramitized di Junit e probabilmente lo userò. Ma devo rispondere con la risposta di dasblinkenlight mentre cercavo una via d'uscita usando i test parametrizzati di Junit – ChrisOdney

+1

Puoi farlo in questo modo, o usare test parametrizzati e usare Assume. Se esiste un metodo di prova che si applica solo a una (o più) classe particolare, allora all'inizio si presuppone che il test venga ignorato per le altre classi. –

+0

Penso che questa sia una base per un'ottima soluzione. È importante poter testare tutti i metodi di interfaccia implementati da un oggetto. Ma immagina un'interfaccia RepositoryGateway con metodi come saveUser (utente) e findUserById (id), che dovrebbero essere implementati per diversi database. Per findUserById (id), l'installazione specifica di testmethod dovrà popolare il database specifico con l'utente da trovare. Per saveUser (utente), la parte assert dovrebbe recuperare i dati dal database specifico. Potrebbe essere risolto aggiungendo un hook (come prepareFindUser) nel metodo di test, implementato dallo specifico testclass? –

0

È possibile creare un metodo di supporto nella classe di test che imposta il test List in modo che un'istanza di una delle implementazioni dipenda da un argomento. In combinazione con this dovresti essere in grado di ottenere il comportamento desiderato.

0

Espansione sulla prima risposta, gli aspetti Parametri di JUnit4 funzionano molto bene. Ecco il codice effettivo che ho usato in un test di un progetto. La classe viene creata utilizzando una funzione di fabbrica (getPluginIO) e la funzione getPluginsNamed ottiene tutte le classi PluginInfo con il nome utilizzando SezPoz e le annotazioni per consentire il rilevamento automatico di nuove classi.

@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 

Nota è importante (io personalmente non hanno alcuna idea perché funziona in questo modo) per avere la collezione come un insieme di array di parametro attuale alimentato al costruttore, in questo caso una classe chiamata PluginInfo. La funzione statica wrapCollection esegue questa attività.

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

Sulla base del anwser di @dasblinkenlight e this anwser mi si avvicinò con un'implementazione per il mio caso d'uso che mi piacerebbe condividere.

Uso lo ServiceProviderPattern (difference API and SPI) per le classi che implementano l'interfaccia IImporterService. Se viene sviluppata una nuova implementazione dell'interfaccia, è necessario modificare solo un file di configurazione in META-INF/services/ per registrare l'implementazione.

Il file in META-INF/services/ prende il nome il nome completo della classe dell'interfaccia di servizio (IImporterService), ad esempio,

de.myapp.importer.IImporterService

Questo file contiene una lista di casses che implementano IImporterService, per esempio

de.myapp.importer.impl.OfficeOpenXMLImporter

La classe factory ImporterFactory fornisce ai clienti implementazioni concrete dell'interfaccia.


Le ImporterFactory restituisce un elenco di tutte le implementazioni dell'interfaccia, registrati attraverso il ServiceProviderPattern. Il metodo setUp() garantisce che venga utilizzata una nuova istanza per ogni caso di 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() { 
    } 
} 

Il metodo ImporterFactory.INSTANCE.getImplementations() è simile al seguente:

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

So che questo è vecchio, ma ho imparato a fare questo in una variante leggermente diversa, che funziona bene in cui è possibile applicare la @Parameter ad un membro del campo per iniettare i valori.

È solo un po 'più pulito a mio parere.

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