为一个接口的多个实现编写单个单元测试



我有一个接口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)注释到您的测试夹具
  • 创建一个返回Collectionpublic 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的答案)。

  1. 为所有测试类创建一个基类:
@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
    

    相关内容

    • 没有找到相关文章

    最新更新