如何为接口编写Junit测试?

89

如何编写JUnit测试接口的最佳方法,以便可以将其用于具体实现类?

例如,您有以下接口和实现类:

public interface MyInterface {
    /** Return the given value. */
    public boolean myMethod(boolean retVal);
}

public class MyClass1 implements MyInterface {
    public boolean myMethod(boolean retVal) {
        return retVal;
    }
}

public class MyClass2 implements MyInterface {
    public boolean myMethod(boolean retVal) {
        return retVal;
    }
}
你如何编写针对接口的测试,以便在类中使用它?
可能性1:
public abstract class MyInterfaceTest {
    public abstract MyInterface createInstance();

    @Test
    public final void testMyMethod_True() {
        MyInterface instance = createInstance();
        assertTrue(instance.myMethod(true));
    }

    @Test
    public final void testMyMethod_False() {
        MyInterface instance = createInstance();
        assertFalse(instance.myMethod(false));
    }
}

public class MyClass1Test extends MyInterfaceTest {
    public MyInterface createInstance() {
        return new MyClass1();
    }
}

public class MyClass2Test extends MyInterfaceTest {
    public MyInterface createInstance() {
        return new MyClass2();
    }
}

优点:

  • 只需实现一个方法

缺点:

  • 测试类的依赖项和模拟对象需要在所有测试中保持一致

可能性2:

public abstract class MyInterfaceTest
    public void testMyMethod_True(MyInterface instance) {
        assertTrue(instance.myMethod(true));
    }

    public void testMyMethod_False(MyInterface instance) {
        assertFalse(instance.myMethod(false));
    }
}

public class MyClass1Test extends MyInterfaceTest {
    @Test
    public void testMyMethod_True() {
        MyClass1 instance = new MyClass1();
        super.testMyMethod_True(instance);
    }

    @Test
    public void testMyMethod_False() {
        MyClass1 instance = new MyClass1();
        super.testMyMethod_False(instance);
    }
}

public class MyClass2Test extends MyInterfaceTest {
    @Test
    public void testMyMethod_True() {
        MyClass1 instance = new MyClass2();
        super.testMyMethod_True(instance);
    }

    @Test
    public void testMyMethod_False() {
        MyClass1 instance = new MyClass2();
        super.testMyMethod_False(instance);
    }
}

优点:

  • 对每个测试进行细粒度的划分,包括依赖和模拟对象

缺点:

  • 每个实现测试类都需要编写额外的测试方法

您更喜欢哪种可能性或您使用的其他方式是什么?


1
@AndyThomas:为什么你这样说?我使用可能性1,将具体类分别放在不同的包和Maven项目中用于实现和测试。 - Trevor Robinson
1
回想起那条三年前的评论,我现在能想到的只有这样一种情况:你无法控制的类可能有多个构造函数,但是可能性1只使用其中一个构造函数创建对象并对其进行测试。 - Andy Thomas
JUnit 5现在支持接口中的测试注释,并且能够正常工作。 - Steven Jeuris
@StevenJeuris 这个问题不是关于向“测试接口”添加注释,而是测试真实接口。 - GMeister
@GMeister 什么是“真正的”接口?在 JUnit 5 中,您可以创建一个接口并向其中添加测试。随后,您可以从接口扩展,并且在接口中指定的测试将在具体类上运行。 - Steven Jeuris
显示剩余3条评论
6个回答

89

与@dlev提供的得到很多赞的答案相反,有时编写像你建议的测试非常有用/必要。通过接口表达的类的公共API是最重要的测试内容。话虽如此,我不会使用你提到的任何一种方法,而是使用Parameterized测试,其中参数是要测试的实现:

@RunWith(Parameterized.class)
public class InterfaceTesting {
    public MyInterface myInterface;

    public InterfaceTesting(MyInterface myInterface) {
        this.myInterface = myInterface;
    }

    @Test
    public final void testMyMethod_True() {
        assertTrue(myInterface.myMethod(true));
    }

    @Test
    public final void testMyMethod_False() {
        assertFalse(myInterface.myMethod(false));
    }

    @Parameterized.Parameters
    public static Collection<Object[]> instancesToTest() {
        return Arrays.asList(
                    new Object[]{new MyClass1()},
                    new Object[]{new MyClass2()}
        );
    }
}

4
这种方法似乎存在问题。所有测试方法都使用同一个 MyClass1 和 MyClass2 实例来执行。理想情况下,每个测试方法应该使用新的 MyClass1/MyClass2 实例来执行,而这个缺陷使得这种方法无法使用。 - ChrisOdney
14
如果您需要为每个测试方法创建一个新的fixture实例,那么请让参数方法返回一个工厂,每个测试都调用该工厂来获取其fixture。这不会影响此方法的可行性。 - Ryan Stewart
1
把类引用放到参数里怎么样?在“InterfaceTesting”方法中使用反射实例化? - ArcanisCz
2
与早期评论者一样,获取要测试的实例的方式并不重要。重要的是某种参数化测试很可能是正确的方法。 - Ryan Stewart
4
假设我编写了一个接口,提供了几个实现和一个测试代码,如果用户创建一个新的实现并想要进行测试,他必须修改测试代码的源代码。这让我认为,返回测试实例的抽象方法更有用,特别是如果您希望客户端创建自己的实现。 - jspurim
显示剩余3条评论

22

我强烈不同意@dlev的观点。在编写测试时使用接口是一种非常好的实践。接口定义了客户端和实现之间的契约。通常情况下,所有的实现都必须通过完全相同的测试。显然,每个实现都可以有自己的测试。

所以,我知道两种解决方案。

  1. 使用带有各种使用接口的测试的抽象测试用例。声明一个返回具体实例的抽象受保护方法。现在,根据需要为接口的每个实现继承此抽象类,并相应地实现所提到的工厂方法。您也可以在此处添加更多特定的测试。

  2. 使用测试套件


16

我也不同意dlev的观点,使用接口编写测试而非具体实现没有问题。

你可能想使用参数化测试。这是使用TestNG的方式,使用JUnit有些矫揉造作(因为你不能直接将参数传递给测试函数):

@DataProvider
public Object[][] dp() {
  return new Object[][] {
    new Object[] { new MyImpl1() },
    new Object[] { new MyImpl2() },
  }
}

@Test(dataProvider = "dp")
public void f(MyInterface itf) {
  // will be called, with a different implementation each time
}

好的答案。看起来TestNG是这个的不错机制。 - fastcodejava

13

对主题的追加,分享较新的解决方案见解

我正在寻找一种适当高效的方法来测试(基于JUnit)某些接口和抽象类的多个实现的正确性。不幸的是,无论是JUnit的@ Parameterized 测试还是TestNG的等价概念都不能正确满足我的要求,因为我不知道这些接口/抽象类的实现列表可能存在的情况。也就是说,可能会开发出新的实现,并且测试人员可能无法访问所有现有实现;因此,测试类指定实现类列表是不高效的。

目前,我已经找到了以下项目,它似乎提供了完整有效的解决方案来简化这种类型的测试:https://github.com/Claudenw/junit-contracts。基本上,它允许通过在合同测试类上使用注释@Contract(InterfaceClass.class)来定义“合同测试” 。然后,一个实现者将创建一个实现特定的测试类,具有注释@RunWith(ContractSuite.class)@ContractImpl(value = ImplementationClass.class);引擎将自动应用与ImplementationClass相关的任何合同测试,通过查找为ImplementationClass的任何接口或抽象类定义的所有合同测试。

我还发现了以下库:http://www.jqno.nl/equalsverifier/。这个库满足了一个类似但更具体的需求,即断言类与Object.equals和Object.hashcode合同的符合性。

同样地,https://bitbucket.org/chas678/testhelpers/src演示了验证一些Java基本合同(包括Object.equals,Object.hashcode,Comparable.compare,Serializable)的策略。该项目使用简单的测试结构,我相信可以轻松复制以适应任何特定需求。

现在就这些,如果我找到其他有用的信息,我会继续更新这篇文章。


5
我通常会避免针对接口编写单元测试,因为接口虽然你希望它能够如此,但实际上并没有定义功能。它只是给其实现者增加了语法要求而已。
相反地,单元测试旨在确保您期望的功能在给定的代码路径中存在。
话虽如此,有些情况下这种类型的测试可能是有意义的。假设您想要这些测试来确保您编写的类(共享给定接口)确实共享相同的功能,则我更喜欢您的第一选择。这使得实现子类最容易将自己注入到测试过程中。此外,我认为您的“缺点”实际上并不正确。没有理由不能让实际上正在进行测试的类自己提供模拟(尽管我认为,如果您真的需要不同的模拟,则表明您的接口测试并不统一)。

30
接口并不定义功能,但是它会定义实现该接口所必需遵循的API。这就是测试应该关注的内容,为什么不编写一个测试来表达这个呢?特别是在使用多种接口实现的多态性时,这种测试非常有价值。 - Ryan Stewart
2
我同意dlev的观点 - 我不认为测试接口有任何意义。编译器会告诉你,如果你的具体实现未能实现接口。我完全看不到它的价值所在。单元测试是针对具体类的。 - duffymo
6
@Ryan - 一个接口协议很少仅仅是“惯例”。协议会明确传达接口的期望。虽然编译器可能允许违反协议,但这通常会导致运行时出现意外行为。在单元测试中只有一个定义该协议的版本比多个版本更好。 - Andy Thomas
1
对于不同意为接口编写单元测试的人 - 如果有多个LinkedList的实现,如Singly、Doubly、Circular等。我为什么要重写通用的单元测试呢?难道使用参数化测试不更方便吗?@duffymo,你是来自sun论坛的那个duffymo吗? - ChrisOdney
5
我只测试合同范围内的接口功能,例如:当第一次将项目添加到列表中时,无论使用哪种实现方式,大小都应变为1,这个不变条件应该保持不变。然而,可能需要对实现进行其他测试,以测试仅针对该实现的功能。 - tsobe
显示剩余4条评论

3

使用Java 8,我这样做:

public interface MyInterfaceTest {
   public MyInterface createInstance();

   @Test
   default void testMyMethod_True() {
       MyInterface instance = createInstance();
       assertTrue(instance.myMethod(true));
   }

   @Test
   default void testMyMethod_False() {
       MyInterface instance = createInstance();
       assertFalse(instance.myMethod(false));
   }
}

public class MyClass1Test implements MyInterfaceTest {
    public MyInterface createInstance() {
        return new MyClass1();
    }
}

public class MyClass2Test implements MyInterfaceTest {
   public MyInterface createInstance() {
       return new MyClass2();
   }

   @Disabled
   @Override
   @Test
   public void testMyMethod_True() {
       MyInterfaceTest.super.testMyMethod_True();
   };
}

"abstract" 关键字是不必要的。 - Ubuntix
这种方法在JUnit 5用户指南中有更详细的记录,位于测试接口和默认方法一节中。 - Geert-Jan Hut

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