我有一个接口List
,其实现包括单链表,双链表,循环等。我为Singly编写的单元测试应该可以很好地用于double、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)
注释到您的测试夹具 - 创建一个返回
Collection
的public static
方法,用@Parameters
注释它,并将SinglyLinkedList.class
,DoublyLinkedList.class
,CircularList.class
等放入该集合 - 为您的测试装置添加一个构造函数,它接受
Class
:public MyListTest(Class cl)
,并将Class
存储在实例变量listClass
中 - 在
setUp
方法或@Before
中,使用List testList = (List)listClass.newInstance();
有了上面的设置,参数化的运行器将为您在@Parameters
方法中提供的每个子类创建测试fixture MyListTest
的新实例,让您对需要测试的每个子类执行相同的测试逻辑。
我知道这是旧的,但我学会了在一个稍微不同的变体中做到这一点,其中您可以将@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的答案和这个答案,我想出了一个我想分享的用例的实现。
对于实现接口IImporterService
的类,我使用ServiceProviderPattern(区别API和SPI)。如果开发了接口的新实现,只需要修改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的Parameter方面工作得非常好。下面是我在测试过滤器的项目中使用的实际代码。该类是使用工厂函数(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