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

64

我有一个接口List,它的实现包括单向链表、双向链表、循环等。我为单向链表编写了单元测试,这些测试对于大多数双向链表、循环以及接口的任何新实现都适用。因此,为了避免为每个实现重复编写单元测试,JUnit提供了内置模块,让我只需编写一个JUnit测试,就可以针对不同的实现运行它吗?

使用JUnit参数化测试,我可以提供不同的实现,如单向链表、双向链表、循环等,但对于每个实现,同一个对象被用于执行类中的所有测试。


1
你是什么意思说“同一个对象用于执行所有测试”? - Olimpiu POP
1
作为一个曾经的JUnit迷,我想说你应该看看Groovy/Spock。Spock很酷,而且Groovy给你一些JUnit无法实现的能力。我最喜欢的之一是可以访问私有数据成员,这样你不必公开某些内容只是为了创建一个适当的单元测试。 - Thom
2
@Thom 你为什么想要访问私有数据成员? - Dávid Horváth
@DávidHorváth 我被分配增加我们的代码覆盖率。我尽可能地试图避免重构测试类。如果他们编写了测试,编写测试就很容易了。:( - Thom
7个回答

72

我建议避免使用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(); 
    }

}

这种做法的好处在于(而不是创建一个测试所有实现的测试类),如果你想要测试某个实现的特定边角情况,你可以只需向特定的测试子类添加更多测试即可。

2
谢谢你的回答,它比 Junit 参数化测试更优雅,我可能会使用它。但是我必须坚持使用 dasblinkenlight 的答案,因为我正在寻找一种使用 Junit 的参数化测试的方法。 - ChrisOdney
1
你可以用这种方式来实现,或者使用参数化测试并使用 Assume。如果有一个测试方法仅适用于一个(或多个)特定类,则应在测试的开头加上 assume,并且该测试将被忽略其他类。 - Matthew Farwell
我认为这是一个伟大解决方案的基础。能够测试对象实现的所有接口方法非常重要。但是想象一下,有一个RepositoryGateway接口,其中包含像saveUser(user)和findUserById(id)这样的方法,应该针对不同的数据库进行实现。对于findUserById(id),测试方法特定的设置将需要使用要查找的用户填充特定的数据库。对于saveUser(user),assert部分应从特定的数据库检索数据。这可以通过在测试方法中添加钩子(例如prepareFindUser),由特定的测试类实现来解决吗? - Jop van Raaij
@JopvanRaaij 这是一种方法,但你也可以将其作为完整的集成测试,并使用接口方法来创建测试对象。 - gustafc
2
为了这个基础测试,我不会使用公共抽象类,而是会使用JUnit5的Test接口(https://junit.org/junit5/docs/current/user-guide/#writing-tests-test-interfaces-and-default-methods),因为它正好可以满足这样的目的。 - Javier Aviles
我有一个使用测试接口的工作示例,正好可以用于这个目的。 https://github.com/javieraviles/fibonacci-calculator/tree/master/src/test/java/com/javieraviles - Javier Aviles

41

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

  • @RunWith(value = Parameterized.class)注释添加到您的测试装置中
  • 创建一个public static方法,返回Collection,用@Parameters进行注释,并将SinglyLinkedList.classDoublyLinkedList.classCircularList.class等放入该集合中
  • 向测试装置添加一个构造函数,该构造函数需要一个Classpublic MyListTest(Class cl),并将该Class存储在实例变量listClass
  • setUp方法或@Before中,使用List testList = (List)listClass.newInstance();

通过以上设置,参数化运行程序将为您提供@Parameters方法中提供的每个子类创建一个新的测试装置MyListTest实例,让您能够对需要测试的每个子类执行相同的测试逻辑。


1
该死!我为什么没想到呢。谢谢。 - ChrisOdney
在setUp方法中执行List testList =(List)listClass.newInstance()如何?而不是在每个测试方法中执行。 - ChrisOdney
很好,感谢。此外,在您的构造函数需要参数时,请查看帖子。 - jaw
3
这种解决方案的一个问题是如果传递的对象需要周围的逻辑来释放资源等,就不清楚这段代码应该位于哪里,因为没有类似于 "@After" 或 "@AfterClass" 的东西。此外,我想知道这是否是对“@Parameterized”的滥用——我想知道它是否应严格用于将简单参数传递给被测试类,而不是作为类本身。我个人有时会使用这种方法,但我认为它有些局限性。 - Dan Gravell
那是一个非常棒的迷你教程。谢谢:D - ThisIsFlorianK
显示剩余2条评论

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

2

基于@dasblinkenlight答案的建议,我开发了一种适用于我的使用情况的实现,并希望分享出来。

我使用 ServiceProviderPatternAPI 和 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);
}

0

对第一个答案进行扩展,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;
}

0
你实际上可以在测试类中创建一个助手方法,该方法根据参数设置你的测试 List 实例来代表你的实现之一。与此同时,结合 this,你应该能够获得你想要的行为。

0

我曾经遇到过完全相同的问题,以下是我的解决方法,借助于 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
    

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接