我有一个接口List
,其实现包括单链表、Doubly、Circular 等。我为 Singly 编写的单元测试应该适用于大多数 Doubly 以及 Circular 和该接口的任何其他新实现。因此,不是为每个实现重复单元测试,JUnit 是否提供了一些内置的东西,让我有一个 JUnit 测试并针对不同的实现运行它?
使用 JUnit 参数化测试,我可以提供不同的实现,如单、双、循环等,但对于每个实现,相同的对象用于执行类中的所有测试。
我有一个接口List
,其实现包括单链表、Doubly、Circular 等。我为 Singly 编写的单元测试应该适用于大多数 Doubly 以及 Circular 和该接口的任何其他新实现。因此,不是为每个实现重复单元测试,JUnit 是否提供了一些内置的东西,让我有一个 JUnit 测试并针对不同的实现运行它?
使用 JUnit 参数化测试,我可以提供不同的实现,如单、双、循环等,但对于每个实现,相同的对象用于执行类中的所有测试。
我可能会避免使用 JUnit 的参数化测试(恕我直言,它的实现非常笨拙),而只是创建一个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();
}
}
这样做的好处(而不是创建一个测试所有实现的测试类)是,如果您想使用一种实现来测试某些特定的极端情况,您可以向特定的测试子类添加更多测试.
使用 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
方法 or@Before
中,使用List testList = (List)listClass.newInstance();
通过上述设置,参数化运行器将为MyListTest
您在@Parameters
方法中提供的每个子类创建一个新的测试夹具实例,让您为需要测试的每个子类执行相同的测试逻辑。
我知道这很旧,但我学会了以稍微不同的变体来做到这一点,这种变体效果很好,您可以将 应用于@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();
}
基于@dasblinkenlight 的anwser和这个anwser,我想出了一个我想分享的用例的实现。
我对实现接口的类使用ServiceProviderPattern(API 和 SPI 的区别IImporterService
) 。如果开发了新的接口实现,只需修改META-INF/services/中的配置文件即可注册实现。
META-INF/services/中的文件以服务接口的全限定类名 ( ) 命名IImporterService
,例如
de.myapp.importer.IImporterService
该文件包含实现的案例列表IImporterService
,例如
de.myapp.importer.impl.OfficeOpenXMLImporter
工厂类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);
}
您实际上可以在测试类中创建一个辅助方法,将您的测试设置List
为依赖于参数的实现之一的实例。结合这个,你应该能够得到你想要的行为。
扩展第一个答案,JUnit4 的参数方面工作得很好。这是我在项目测试过滤器中使用的实际代码。该类是使用工厂函数 ( getPluginIO
) 创建的,该函数getPluginsNamed
使用 SezPoz 和注释获取所有具有名称的 PluginInfo 类,以允许自动检测新类。
@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;
}
我遇到了完全相同的问题,这是我借助 JUnit
参数化测试的方法(基于@dasblinkenlight 的回答)。
@RunWith(value = Parameterized.class)
public class ListTestUtil {
private Class<?> listClass = null;
public ListTestUtil(Class<?> listClass) {
this.listClass = listClass;
}
/**
* @return a {@link Collection} with the types of the {@link List} implementations.
*/
@Parameters
public static Collection<Class<?>> getTypesData() {
return List.of(MySinglyLinkedList.class, MyArrayList.class);
}
public <T> List<Integer> initList(Object... elements) {
return initList(Integer.class, elements);
}
@SuppressWarnings("unchecked")
public <T> List<T> initList(Class<T> type, Object... elements) {
List<T> myList = null;
try {
myList = (List<T>) listClass.getDeclaredConstructor().newInstance();
for (Object el : elements)
myList.add(type.cast(el));
} catch (Exception e) {
e.printStackTrace();
}
return myList;
}
}
ListTestUtil
,您可以在initList(...)
任何地方使用:public class AddTest extends ListTestUtil {
public AddTest(Class<?> cl) {
super(cl);
}
@Test
public void test1() {
List<Integer> myList = initList(1, 2, 3);
// List<Integer> myList = initList(Strng.class, "a", "b", "c");
...
System.out.println(myList.getClass());
}
}
输出证明测试被调用了两次——列表的每个实现一次:
class java.data_structures.list.MySinglyLinkedList
class java.data_structures.list.MyArrayList