Запись одного 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;
}