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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/16237135/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-31 22:20:23  来源:igfitidea点击:

Writing a single unit test for multiple implementations of an interface

javaunit-testingjunitjunit4junit-runner

提问by ChrisOdney

I have an interface Listwhose implementations include Singly Linked List, Doubly, Circular etc. The unit tests I wrote for Singly should do good for most of Doubly as well as Circular and any other new implementation of the interface. So instead of repeating the unit tests for every implementation, does JUnit offer something inbuilt which would let me have one JUnit test and run it against different implementations?

我有一个接口,List它的实现包括单链表、双链表、循环等。我为单链编写的单元测试应该对大多数双链表以及循环和接口的任何其他新实现都有好处。因此,不是为每个实现重复单元测试,JUnit 是否提供了一些内置的东西,让我有一个 JUnit 测试并针对不同的实现运行它?

Using JUnit parameterized tests I can supply different implementations like Singly, doubly, circular etc but for each implementation the same object is used to execute all the tests in the class.

使用 JUnit 参数化测试,我可以提供不同的实现,如 Singly、double、circular 等,但对于每个实现,使用相同的对象来执行类中的所有测试。

采纳答案by dasblinkenlight

With JUnit 4.0+ you can use parameterized tests:

使用 JUnit 4.0+,您可以使用参数化测试

  • Add @RunWith(value = Parameterized.class)annotation to your test fixture
  • Create a public staticmethod returning Collection, annotate it with @Parameters, and put SinglyLinkedList.class, DoublyLinkedList.class, CircularList.class, etc. into that collection
  • Add a constructor to your test fixture that takes Class: public MyListTest(Class cl), and store the Classin an instance variable listClass
  • In the setUpmethod or @Before, use List testList = (List)listClass.newInstance();
  • @RunWith(value = Parameterized.class)向您的测试装置添加注释
  • 创建一个public static方法返回Collection,带有注释它@Parameters,并把SinglyLinkedList.classDoublyLinkedList.classCircularList.class等进入该集合
  • 向您的测试装置添加一个构造函数,它采用Class: public MyListTest(Class cl),并将其存储Class在一个实例变量中listClass
  • setUp方法 or 中@Before,使用List testList = (List)listClass.newInstance();

With the above setup in place, the parameterized runner will make a new instance of your test fixture MyListTestfor each subclass that you provide in the @Parametersmethod, letting you exercise the same test logic for every subclass that you need to test.

完成上述设置后,参数化运行器将为MyListTest您在@Parameters方法中提供的每个子类创建一个新的测试装置实例,让您对需要测试的每个子类执行相同的测试逻辑。

回答by gustafc

I'd probably avoid JUnit's parameterized tests (which IMHO are pretty clumsily implemented), and just make an abstract Listtest class which could be inherited by tests implementations:

我可能会避免使用 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(){ /* ... */ }

}

The different implementations then get their own concrete classes:

不同的实现然后得到它们自己的具体类:

class SinglyLinkedListTest extends ListTestBase<SinglyLinkedList> {

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

}

class DoublyLinkedListTest extends ListTestBase<DoublyLinkedList> {

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

}

The nice thing about doing it this way (instead of making one test class which tests all implementations) is that if there are some specific corner cases you'd like to test with one implementation, you can just add more tests to the specific test subclass.

这样做的好处(而不是制作一个测试所有实现的测试类)是,如果您想用一个实现测试某些特定的极端情况,您可以向特定的测试子类添加更多测试.

回答by mcnichol

I know this is old, but I learned to do this in a slightly different variation which works nicely wherein you can apply the @Parameterto a field member to inject the values.

我知道这是旧的,但我学会了以稍微不同的变体来做到这一点,该变体效果很好,您可以将 应用于@Parameter字段成员以注入值。

It's just a little cleaner in my opinion.

在我看来,它只是更清洁一点。

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

回答by Matthias Holdorf

Based on the anwser of @dasblinkenlightand thisanwser I came up with an implementation for my use case that I'd like to share.

基于@dasblinkenlight 的anwser这个anwser,我想出了一个我想分享的用例的实现。

I use the ServiceProviderPattern(difference API and SPI) for classes that implement the interface IImporterService. If a new implementation of the interface is developed, only a configuration file in META-INF/services/needs to be altered to register the implementation.

我将ServiceProviderPattern不同的 API 和 SPI)用于实现该接口的类IImporterService。如果开发了新的接口实现,只需更改META-INF/services/ 中的配置文件即可注册该实现。

The file in META-INF/services/is named after the fully qualified class name of the service interface (IImporterService), e.g.

META-INF/services/ 中的文件以服务接口的全限定类名 ( IImporterService)命名,例如

de.myapp.importer.IImporterService

de.myapp.importer.IImporterService

This file contains a list of casses that implement IImporterService, e.g.

此文件包含实现 的案例列表IImporterService,例如

de.myapp.importer.impl.OfficeOpenXMLImporter

de.myapp.importer.impl.OfficeOpenXMLImporter

The factory class ImporterFactoryprovides clients with concrete implementations of the interface.

工厂类ImporterFactory为客户端提供接口的具体实现。



The ImporterFactoryreturns a list of all implementations of the interface, registered via the ServiceProviderPattern. The setUp()method ensures that a new instance is used for each test case.

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

The ImporterFactory.INSTANCE.getImplementations()method looks like the following:

ImporterFactory.INSTANCE.getImplementations()方法如下所示:

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

回答by LionC

You could actually create a helper method in your test class that sets up your test Listto be an instance of one of your implementations dependent on an argument. In combination with thisyou should be able to get the behaviour you want.

您实际上可以在您的测试类中创建一个辅助方法,将您的测试设置List为依赖于参数的实现之一的实例。与相结合,您应该能够获得所需的行为。

回答by kmader

Expanding on the first answer, the Parameter aspects of JUnit4 work very well. Here is the actual code I used in a project testing filters. The class is created using a factory function (getPluginIO) and the function getPluginsNamedgets all PluginInfo classes with the name using SezPoz and annotations to allow for new classes to be automatically detected.

扩展第一个答案,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

Note it is important (I personally have no idea why it works this way) to have the collection as a collection of arrays of the actual parameter fed to the constructor, in this case a class called PluginInfo. The wrapCollection static function performs this task.

请注意,将集合作为提供给构造函数的实际参数数组的集合很重要(我个人不知道为什么会这样),在这种情况下是一个名为 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;
}