如何编写接口的junit测试?


77

为接口编写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是不够的。
安迪·托马斯

1
@AndyThomas:为什么这么说?我将可能性1与不同包和Maven项目中的具体类(用于实现和测试)一起使用。
Trevor Robinson

1
@TrevorRobinson-回想起已有三年历史的评论,目前我能想到的是,您控制范围之外的类可能具有多个构造函数,但可能性1对仅使用其中一个创建的对象运行每个测试。
安迪·托马斯

使用option1,每个具体的类中可以有单独的@Before方法。您也可以根据需要在具体课程中进行一次性测试。
JoshOrndorff

Answers:


79

与@dlev给出的备受争议的答案相反,有时候像您所建议的那样编写测试可能非常有用/很有必要。通过其接口表示的类的公共API是最重要的测试对象。话虽如此,我不会使用您提到的任何方法,而是使用参数化测试,其中参数是要测试的实现:

@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的相同实例用于执行所有测试方法。理想情况下,每个testMethod都应使用MyClass1 / MyClass2的新实例执行,此缺点使此方法不可用。
ChrisOdney

12
如果每个测试方法都需要一个新的夹具实例,那么让参数方法返回一个工厂,每个测试都会调用该工厂来获取其夹具。它不会影响这种方法的可行性。
瑞安·斯图尔特

1
如何将类引用放入参数,并在“ InterfaceTesting”方法中使用反射实例化?
ArcanisCz 2013年

2
@ArcanisCz:与伯爵评论者一样,如何让实例进行测试并不重要。重要的是某种参数化测试可能是正确的方法。
瑞安·斯图尔特

4
假设我写了一个接口,提供了一些实现和一个像这样编写的测试。如果用户创建了一个新的实现并想要对其进行测试,则他必须修改测试的源代码。这使我认为返回测试实例的抽象方法更有用,尤其是在希望客户创建自己的实现的情况下。
jspurim

20

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

因此,我知道2个解决方案。

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

  2. 使用测试套件


14

我也不同意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 2011年

13

主题的后期添加,分享新的解决方案见解

我也在寻找一种正确有效的方法(基于JUnit)测试某些接口和抽象类的多个实现的正确性。不幸的是,JUnit的@Parameterized测试和TestNG的等效概念都不能正确地满足我的要求,因为我不知道这些接口/抽象类的实现的先验清单。也就是说,可能会开发新的实现,并且测试人员可能无法访问所有现有的实现。因此,让测试类指定实现类列表是没有效率的。

至此,我发现以下项目似乎为简化此类测试提供了完整而有效的解决方案:https : //github.com/Claudenw/junit-contracts。它基本上允许通过@Contract(InterfaceClass.class)合同测试类的注释来定义“合同测试” 。然后,实现者将创建带有注释@RunWith(ContractSuite.class)@ContractImpl(value = ImplementationClass.class);的。引擎应通过查找为实现类继承的任何接口或抽象类定义的所有合同测试,来自动应用适用于ImplementationClass的任何合同测试。我尚未测试此解决方案,但这听起来很有希望。

我还找到了以下库:http : //www.jqno.nl/equalsverifier/。这满足了一个相似但更具体的需求,即断言特定于Object.equals和Object.hashcode契约的类一致性。

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

好吧,仅此而已;我将用可能会发现的其他有用信息来更新此帖子。


6

我通常会避免针对接口编写单元测试,原因很简单:接口(无论您多么希望它)都没有定义功能。它给实现者带来了句法要求,仅此而已。

相反,单元测试旨在确保给定的代码路径中具有您期望的功能。

话虽这么说,在某些情况下这种类型的测试可能有意义。假设您想要这些测试来确保您编写的(共享给定接口的)类确实共享相同的功能,那么我希望您选择第一个方法。这使实现子类最容易将其自身注入测试过程。另外,我不认为您的“骗局”是真的。没有理由您不能让实际上正在测试的类提供它们自己的模拟(尽管我认为如果您确实需要不同的模拟,那么这表明您的接口测试始终不是统一的。)


27
接口未定义功能,但定义了其实现必须遵守的API。那就是测试应该关注的重点,那么为什么不编写一个测试来表达这一点呢?特别是当您充分利用接口的多种实现的多态性时,这种测试非常有价值。
瑞安·斯图尔特

2
我同意dlev-在测试界面上没有意义。编译器会告诉您您的具体实现是否无法实现该接口。我认为它没有任何价值。单元测试适用于具体课程。
duffymo 2011年

5
@Ryan-接口合同很少是“仅是约定”。合同传达了接口接受者的坚定期望。编译器可能允许违反合同,但通常会在运行时导致意外行为。在单元测试中对该合同的单个定义要好于多个。
安迪·托马斯

1
对于不同意编写接口的单元测试的人-如果LinkedList有多个实现,例如Singly,Doubly,Circular等。为什么我应该重写常见的单元测试,所以有参数化测试不是更方便吗?@duffymo您是来自Sun论坛的同一个duffymo吗?
ChrisOdney

5
我将仅通过接口测试作为合同一部分的功能,例如:第一次添加到列表中时,大小应为1,无论使用哪种实现,都应保持不变。但是,实现可能需要其他测试,这些测试仅测试特定于该实现的功能。
2016年

1

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

“抽象”关键字是不必要的。
Ubuntix

在《 JUnit 5用户指南》的“测试接口和默认方法”部分中,对该方法进行了更详尽的记录。
盖特·简·胡特
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.