2013-04-26 14 views
40

Mam interfejs List, którego implementacje obejmują listę pojedynczo połączoną, podwójną, okrągłą itd. Testy jednostkowe, które napisałem dla Singly, powinny czynić dobro dla większości podwójnych, a także okrągłych i każda inna nowa implementacja interfejsu. Więc zamiast powtarzać testy jednostkowe dla każdej implementacji, JUnit oferuje coś wbudowanego, co pozwoliłoby mi przeprowadzić jeden test JUnit i uruchomić go na różnych implementacjach?Pisanie testu pojedynczej jednostki dla wielu implementacji interfejsu

Przy użyciu testów sparametryzowanych JUnit mogę dostarczyć różne implementacje, takie jak Singly, podwójnie, okrągłe itp., Ale dla każdej implementacji ten sam obiekt służy do wykonywania wszystkich testów w klasie.

+0

co masz na myśli "ten sam obiekt służy do wykonywania wszystkich testów"? –

+0

Jako były narkoman, chciałbym powiedzieć, że powinieneś spojrzeć na groovy/spock. Spock jest fajny i groovy daje ci pewne umiejętności, których po prostu nie możesz zrobić na junicie. Jedną z moich ulubionych rzeczy jest dostęp do prywatnych danych, więc nie musisz ujawniać czegoś tylko po to, by stworzyć odpowiedni test jednostkowy. – Thom

Odpowiedz

31

Z JUnit 4.0+ można wykorzystać parameterized tests:

  • Dodaj @RunWith(value = Parameterized.class) adnotacji do przyrządu
  • Tworzenie public static metodę powracającego Collection, adnotacje go @Parameters i umieścić SinglyLinkedList.class, DoublyLinkedList.class, CircularList.class itp do tej kolekcji
  • Dodaj konstruktora do swojego urządzenia testowego, który zajmuje Class: public MyListTest(Class cl) i zapisz Class w instancji zmiennej listClass
  • W metodzie setUp lub @Before użyć List testList = (List)listClass.newInstance();

Z powyższej konfiguracji na miejscu, parametryzowane biegacz utworzy nową instancję swojej przyrządu MyListTest dla każdej podklasy, które dostarczają w metodzie @Parameters, pozwalającej na ćwiczenie tej samej logiki testowej dla każdej podklasy, którą należy przetestować.

+0

Cholera! Dlaczego o tym nie myślałem. Dzięki. – ChrisOdney

+0

Co powiesz na wykonanie List testList = (List) listClass.newInstance(); w metodzie setUp zamiast każdej metody testowej? – ChrisOdney

+0

@ChrisOdney Tak, to też by działało. Można również utworzyć pomocniczą metodę 'makeList()' na wypadek, gdyby niektóre z twoich metod testowych musiały utworzyć kilka wystąpień klasy listy. – dasblinkenlight

42

pewnie bym uniknąć parametryzowane testy JUnit (który IMHO są dość nieudolnie realizowane), i po prostu zrobić abstrakcyjne List klasy testowej, które mogą być dziedziczone przez testy implementacji:

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

} 

Różne implementacje następnie uzyskać ich własnych konkretnych klas:

class SinglyLinkedListTest extends ListTestBase<SinglyLinkedList> { 

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

} 

class DoublyLinkedListTest extends ListTestBase<DoublyLinkedList> { 

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

} 

zaletą robi to w ten sposób (zamiast tworzyć jedną klasę testową, która testuje wszystkie implementacje) jest to, że jeśli istnieją pewne szczególne przypadki narożne chcesz test z jedną implementacją, możesz po prostu dodać więcej testów do określonej podklasy testowej.

+2

Dzięki za odpowiedź, bardziej elegancka niż testy Junit Paramterized i prawdopodobnie go użyję. Ale muszę trzymać się odpowiedzi dasblinkenlight, gdy szukałem sposobu na wykorzystanie sparametryzowanych testów Junit. – ChrisOdney

+1

Można to zrobić w ten sposób lub użyć sparametryzowanych testów i użyć Załóżmy. Jeśli istnieje metoda testowa, która ma zastosowanie tylko do jednej (lub więcej) konkretnej klasy, to na początku testu można założyć, a test zostanie zignorowany dla innych klas. –

+0

Myślę, że to podstawa świetnego rozwiązania. Ważne jest, aby móc przetestować wszystkie metody interfejsu implementowane przez obiekt. Ale wyobraź sobie interfejs RepositoryGateway z takimi metodami jak saveUser (user) i findUserById (id), które powinny być zaimplementowane dla różnych baz danych. Dla findUserById (id), specyficzna konfiguracja testmethod będzie wymagać wypełnienia konkretnej bazy danych, w której znajduje się użytkownik. Dla saveUser (użytkownik), asercja powinna pobierać dane z konkretnej bazy danych. Czy można to rozwiązać przez dodanie haka (np. PrepareFindUser) w metodzie testowej, zaimplementowanej przez konkretną testclass? –

0

W rzeczywistości można utworzyć metodę pomocniczą w klasie testowej, która ustawia test jako jedną z implementacji zależną od argumentu. W połączeniu z this powinieneś być w stanie uzyskać pożądane zachowanie.

0

Rozszerzając pierwszą odpowiedź, aspekty parametrów JUnit4 działają bardzo dobrze. Oto rzeczywisty kod użyty w filtrach testowych projektu. Klasa jest tworzona przy użyciu funkcji fabrycznej (getPluginIO), a funkcja getPluginsNamed pobiera wszystkie klasy PluginInfo o nazwie przy użyciu SezPoz i adnotacji, aby umożliwić automatyczne wykrywanie nowych klas.

@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 

Uwaga ważne jest (ja osobiście nie mam pojęcia, dlaczego to działa w ten sposób), tak aby kolekcję jako zbiór tablic rzeczywistego parametru podawanego do konstruktora, w tym przypadku klasy o nazwie PluginInfo. Funkcja statyczna wrapCollection wykonuje to zadanie.

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

podstawie anwser z @dasblinkenlight i this anwser wymyśliłem implementacji dla mojego przypadku użycia, że ​​chciałbym się podzielić.

Używam ServiceProviderPattern (difference API and SPI) dla klas implementujących interfejs IImporterService. Jeśli zostanie opracowana nowa implementacja interfejsu, tylko plik konfiguracyjny w META-INF/services/ musi zostać zmieniony, aby zarejestrować implementację.

Plik w META-INF/services/ pochodzi od pełnej nazwy klasy interfejsu serwisowego (IImporterService), na przykład

de.myapp.importer.IImporterService

Ten plik zawiera listę casses realizujących IImporterService np

de.myapp.importer.impl.OfficeOpenXMLImporter

Klasa fabryka ImporterFactory zapewnia klientom konkretnych implementacji interfejsu.


W ImporterFactory Zwraca listę wszystkich implementacje interfejsu, zarejestrowanych przez ServiceProviderPattern. Metoda setUp() zapewnia użycie nowej instancji dla każdego przypadku testowego.

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

Sposób ImporterFactory.INSTANCE.getImplementations() wygląda następująco:

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

wiem, że to jest stary, ale ja nauczyłem się robić to w nieco innej odmianie, która działa ładnie w którym można zastosować @Parameter się do członek pola, aby wprowadzić wartości.

To tylko trochę czystsze w mojej opinii.

@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(); 
    } 
Powiązane problemy