Запись одного unit test для нескольких реализаций интерфейса

У меня есть интерфейс List, реализация которого включает Singlely Linked List, Doubly, Circular и т.д. Модульные тесты, которые я написал для Singly, должны делать добро для большей части Doubly, а также Circular и любой другой новой реализации интерфейса. Итак, вместо того, чтобы повторять модульные тесты для каждой реализации, делает ли JUnit что-то встроенное, что позволило бы мне провести один тест JUnit и запустить его для разных реализаций?

Используя JUnit-параметризованные тесты, я могу поставлять различные реализации, такие как Singly, doublely, round и т.д., но для каждой реализации один и тот же объект используется для выполнения всех тестов в классе.

Ответ 1

С JUnit 4.0+ вы можете использовать параметризованные тесты:

  • Добавить @RunWith(value = Parameterized.class) аннотацию к вашему тестовому прибору
  • Создайте метод public static, возвращающий Collection, аннотируйте его с помощью @Parameters и поместите SinglyLinkedList.class, DoublyLinkedList.class, CircularList.class и т.д. в эту коллекцию
  • Добавьте конструктор в тестовое устройство, которое принимает Class: public MyListTest(Class cl), и сохраните Class в переменной экземпляра listClass
  • В методе setUp или @Before используйте List testList = (List)listClass.newInstance();

При установленной выше настройке параметризованный бегун сделает новый экземпляр вашего тестового прибора MyListTest для каждого подкласса, который вы предоставляете в методе @Parameters, позволяя вам выполнять ту же логику теста для каждого подкласса, который вы необходимо проверить.

Ответ 2

Я бы, вероятно, избегал параметризованных тестов JUnit (которые IMHO довольно неуклюже реализованы), и просто создайте абстрактный тестовый класс List, который может быть унаследован реализацией тестов:

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

}

Различные реализации затем получают свои собственные конкретные классы:

class SinglyLinkedListTest extends ListTestBase<SinglyLinkedList> {

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

}

class DoublyLinkedListTest extends ListTestBase<DoublyLinkedList> {

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

}

Хорошая вещь в том, чтобы делать это таким образом (вместо того, чтобы делать один тестовый класс, который проверяет все реализации), состоит в том, что если есть некоторые конкретные угловые случаи, которые вы хотели бы протестировать с одной реализацией, вы можете просто добавить больше тестов для специфический тестовый подкласс.

Ответ 3

Я знаю, что это старо, но я научился делать это несколько иначе, что прекрасно работает, когда вы можете применить @Parameter к члену поля, чтобы ввести значения.

Это немного чище, на мой взгляд.

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

Ответ 4

Основываясь на anwser @dasblinkenlight и this anwser, я придумал реализацию для моего использования который я хотел бы поделиться.

Я использую ServiceProviderPattern (API различий и SPI) для классов, которые реализовать интерфейс IImporterService. Если новая реализация интерфейса разработана, для регистрации реализации необходимо изменить только файл конфигурации в META-INF/services/.

Файл в META-INF/services/ имеет имя после полного имени класса сервисного интерфейса (IImporterService), например.

de.myapp.importer.IImporterService

Этот файл содержит список касс, которые реализуют IImporterService, например.

de.myapp.importer.impl.OfficeOpenXMLImporter

Класс factory ImporterFactory предоставляет клиентам конкретные реализации интерфейса.


ImporterFactory возвращает список всех реализаций интерфейса, зарегистрированных через ServiceProviderPattern. Метод setUp() гарантирует, что для каждого тестового примера используется новый экземпляр.

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

Метод ImporterFactory.INSTANCE.getImplementations() выглядит следующим образом:

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

Ответ 5

Фактически вы можете создать вспомогательный метод в своем тестовом классе, который устанавливает ваш тест List как экземпляр одной из ваших реализаций, зависящих от аргумента. В сочетании с этим вы сможете получить нужное поведение.

Ответ 6

Расширяясь в первом ответе, аспекты параметра JUnit4 работают очень хорошо. Вот фактический код, который я использовал в фильтрах тестирования проекта. Класс создается с помощью функции factory (getPluginIO), а функция getPluginsNamed получает все классы PluginInfo с именем, использующим SezPoz и аннотации, позволяющие автоматически определять новые классы.

@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

Заметьте, что важно (я лично не знаю, почему он работает таким образом), чтобы коллекция представляла собой набор массивов фактического параметра, переданного конструктору, в этом случае класс под названием PluginInfo. Статическая функция wrapCollection выполняет эту задачу.

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