【问题标题】:How to write junit tests for interfaces?如何为接口编写junit测试?
【发布时间】:2011-07-17 14:15:18
【问题描述】:

为接口编写 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;
    }
}

您将如何针对接口编写测试,以便将其用于类?

可能性一:

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:你为什么这么说?我在不同的包和 Maven 项目中将可能性 1 与具体类(用于实现和测试)一起使用。
  • @TrevorRobinson - 回想那个三年前的评论,我现在能想到的就是你无法控制的类可能有多个构造函数,但可能性 1 在一个仅使用其中之一创建的对象。
  • 使用选项1,您可以在每个具体类中拥有单独的@Before 方法。您还可以根据需要在具体课程中进行一次性测试。

标签: java unit-testing testing interface junit


【解决方案1】:

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

【讨论】:

  • 这种方法似乎有问题。 MyClass1 和 MyClass2 的同一个实例用于执行所有测试方法。理想情况下,每个 testMethod 都应该使用 MyClass1/MyClass2 的新实例来执行,这个缺点使这种方法无法使用。
  • 如果您需要为每个测试方法创建一个新的夹具实例,那么让参数方法返回一个工厂,每个测试调用该工厂以获取其夹具。它不会影响这种方法的可行性。
  • 如何将类引用放到参数中,并在“InterfaceTesting”方法中用反射实例化?
  • @ArcanisCz:与前面的评论者一样,如何让实例进行测试并不重要。重要的一点是,某种参数化测试可能是正确的方法。
  • 假设我写了一个接口,提供一些实现和一个这样写的测试。如果用户创建了一个新的实现并想要对其进行测试,他必须修改测试的源代码。这让我认为返回测试实例的抽象方法更有用,特别是如果您希望您的客户创建自己的实现。
【解决方案2】:

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

所以,我知道 2 个解决方案。

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

  2. 使用test suites

【讨论】:

    【解决方案3】:

    我也不同意 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 很好的机制。
    【解决方案4】:

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

    我还在寻找一种适当且有效的方法来测试某些接口和抽象类的多个实现的正确性(基于 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】:

      我通常会避免针对接口编写单元测试,原因很简单,接口,无论你多么喜欢它,并没有定义功能。它给实现者带来了语法要求,但仅此而已。

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

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

      【讨论】:

      • 接口不定义功能,但它定义了其实现必须遵守的API。这就是测试应该关注的内容,那么为什么不写一个测试来表达这一点呢?特别是当您在一个接口的多个实现中充分利用多态性时,这种测试非常有价值。
      • 我同意 dlev - 我认为测试接口没有意义。如果您的具体实现未能实现接口,编译器会告诉您。我认为它没有任何价值。单元测试是针对具体类的。
      • @Ryan - 接口契约很少“只是一个约定”。合同传达了接口接收者的坚定期望。编译器可能允许违反合约,但会经常在运行时导致意外行为。在单元测试中这个契约的单一定义比多个更好。
      • 对于不同意为接口编写单元测试的人 - 如果 LinkedList 有多种实现,例如单、双、循环等。为什么我要重写常见的单元测试,不是更方便进行参数化测试吗? @duffymo 你是来自 sun 论坛的同一个 duffymo 吗?
      • 我将通过接口仅测试属于合同一部分的功能,例如:当项目第一次添加到列表中时,size 应该变成 1,无论使用哪个实现,这个不变量都应该成立。但是,实现可能需要额外的测试,这些测试仅测试特定于该实现的功能。
      【解决方案6】:

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

      【讨论】:

      猜你喜欢
      • 2021-02-03
      • 2022-11-04
      • 2022-11-03
      • 2013-01-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多