【问题标题】:Best Practice: Testing Interface Contract Compliance?最佳实践:测试接口合同合规性?
【发布时间】:2013-01-08 08:30:50
【问题描述】:

假设定义了接口

interface Foo {
  int getBaz();
  void doBar();
}

进一步假设合同规定每次调用 doBar 时 baz 都会递增。 (好吧,这是一段人为的代码,但请坚持我)

现在我想提供一个单元测试,我可以提供给 Foo 实现者,以便他们可以验证他们是否满足所有合同条件。

class FooTest {
  protected Foo fooImpl;

  @Test
  public void int testBazIncrement()
  {
    int b = fooImpl getBaz();
    fooImpl.doBar();
    Assert.assertEquals( b+1, fooImpl.getBaz();
  }
}

将测试提供给 Foo 的实施者的最佳实践是什么?在我看来,他们需要一种机制来调用 FooTest 并提供 Foo 或 FooFactory 来构造 Foo 实例。此外,如果有很多测试(想想大接口),那么我想将所有这些测试放在一个 FooTest 类中。

对于如何实施此类测试,是否有任何最佳实践?

【问题讨论】:

    标签: java testing interface junit


    【解决方案1】:

    这是一个教科书式的依赖注入示例。如果你使用 Spring 作为 DI 容器,你可以在 fooImpl 中接线

    @Inject
    protected Foo fooImpl;
    

    您的测试需要使用 @RunWith(SpringJUnit4ClassRunner.class) 进行注释,并且由接口提供者来配置 Spring 及其实现。

    如果没有 DI 容器,您可以创建一个包含所有测试的抽象测试类和一个提供实现对象的抽象方法。

    protected abstract Foo createFoo();
    

    由实现提供者对测试进行子类化并实现抽象方法。

    class FooImplTest extends FooTestSuper {
    
    @Override
    protected Foo createFoo() {
        return new FooImpl();
    }
    

    如果您有多个测试,请考虑 JUnit 的 @Suite 注释。它与 Spring 测试运行器兼容。

    【讨论】:

    • 如果您使用依赖注入为每个项目提供每个接口的单个​​实现,那没关系。它是否适用于更像 Serializable 接口的情况,许多事情必须正确实现'?
    • 你可以拥有尽可能多的 Spring 配置,只要有实现
    • 不幸的是,创建所有这些配置可能比在代码中提供解决方案更容易出错......
    • @Soru 使用 Spring Java 配置(无 XML),这很容易。
    • 这可能适用于使用 IoC 框架的项目,并且由于我没有指定 IoC 未用于该项目,这似乎是一个很好且有效的答案。但是,在我的具体情况下,我们并没有将 IoC 用于项目本身,而仅添加 IoC 用于测试似乎有点多。
    【解决方案2】:

    您可以实现一个 testDataFactory 来定义您的对象,或者使用 GSon 来创建您的对象(我个人喜欢 GSon,它清晰且快速,您可以在一段时间内学会它)。 对于测试实现,我建议编写更多测试而不是一个。 通过这种方式,单元测试可以是独立的,您可以将您的问题隔离在一个封闭的结构中。

    Sonar

    Sonar 是一种对您有很大帮助的工具,可以对您的代码进行分析。您可以从声纳前端看到您的应用程序是如何测试的:

    sonar unit test

    如您所见,Sonar 可以显示您的代码在哪里进行了测试

    【讨论】:

    • 如果有多个测试用例类,实现者如何知道他们已经在他们的测试中包含了所有相关的测试?如果我后来发现我错过了一个测试用例并为其添加测试,实施者怎么知道?如果测试在一个文件中,那么他们将在下一个版本中选择新的测试。
    • 对不起,点击错误..我删除了我之前的评论!我编辑了回复。就个人而言,我只测试我的应用程序的核心,当我编写一个内部有逻辑的函数时,这部分需要测试。查询数据库的功能也需要测试,这样一来,如果你犯了一些错误,你可以立即得到响应。
    • 你最后选择了什么选项?
    【解决方案3】:

    为什么不只拥有一个从单元测试InterfaceImplATestInterfaceImplBTest等中调用的InterfaceTester

    例如

    @Test
    public void testSerialisation()
    {
      MyObject a = new MyObject();
    
      ...
    
      serialisationTester.testSimpleRoundTrip(a);
    
      serialisationTester.testEdgeCases(a);
    
      ... 
    }
    

    【讨论】:

    • 您从中获得的诊断肯定会很糟糕吗?
    • 据我所知,不比任何其他junit测试多或少。如果您确实需要比“序列化不适用于 MyObject”更详细的信息,请拆分为更小的 @Test。
    • 这个测试用例的问题是MyObject的实现需要为每个子类改变。
    【解决方案4】:

    经过深思熟虑和一些死胡同,我开始使用以下模式:

    如下:

    • [INTERFACE] 指的是正在测试的接口。
    • [CLASS] 是指被测试接口的实现。

    已构建接口测试,以便开发人员可以测试实现是否符合合同 在界面和随附文档中列出。

    被测试的主要项目使用 [INTERFACE]ProducerInterface 的实例来创建被测试对象的实例。 [INTERFACE]ProducerInterface 的实现必须跟踪测试期间创建的所有实例,并在请求时关闭所有实例。有一个 Abstract[INTERFACE]Producer 可以处理大部分功能,但需要 createNewINTERFACE 实现。

    测试

    接口测试被标记为 Abstract[INTERFACE]Test。测试通常扩展 Abstract[INTERFACE]ProducerUser 类。此类处理在测试结束时清理所有图表,并为实现者提供一个挂钩以插入他们的 [INTERFACE]ProducerInterface 实现。

    一般来说,实现一个测试需要几行代码,如下例所示 正在测试新的 Foo 图实现。

    
    public class FooGraphTest extends AbstractGraphTest {
    
            // the graph producer to use while running
            GraphProducerInterface graphProducer = new FooGraphTest.GraphProducer();
    
            @Override
            protected GraphProducerInterface getGraphProducer() {
                    return graphProducer;
            }
    
            // the implementation of the graph producer.
            public static class GraphProducer extends AbstractGraphProducer {
                    @Override
                    protected Graph createNewGraph() {
                            return new FooGraph();
                    }
            }
    
    }
    

    套房

    测试套件被命名为 Abstract[INTERFACE]Suite。套件包含几个测试,用于对被测对象的组件进行所有测试。例如,如果 Foo.getBar() 返回 Bar 接口的实例,则 Foo 套件包括对 Foo 本身的测试以及运行 Bar 测试 Bar。运行套件比运行测试要复杂一些。

    套件是使用 JUnit 4 @RunWith(Suite.class) 和 @Suite.SuiteClasses({ }) 创建的 注释。这有几个影响开发者应该知道的:

    1. 套件类在运行期间未实例化。
    2. 测试类名称必须在编码时(而不是运行时)已知,因为它们在注释中列出。
    3. 测试的配置必须在类加载的静态初始化阶段进行。

    为了满足这些要求,Abstract[INTERFACE]Suite 有一个静态变量,用于保存 [INTERFACE]ProducerInterface 的实例和一些实现“get[INTERFACE]Producer()”的抽象测试的本地静态实现通过返回静态实例的方法。然后在 @Suite.SuiteClasses 注释中使用本地测试的名称。这使得为​​ [INTERFACE] 实现创建 Abstract[INTERFACE]Suite 的实例相当简单,如下所述。

    
    public class FooGraphSuite extends AbstractGraphSuite {
    
            @BeforeClass
            public static void beforeClass() {
                    setGraphProducer(new GraphProducer());
            }
    
            public static class GraphProducer extends AbstractGraphProducer {
                    @Override
                    protected Graph createNewGraph() {
                            return new FooGraph();
                    }
            }
    
    }
    

    注意 beforeClass() 方法是用@BeforeClass 注解的。 @BeforeClass 导致它在类中的任何测试方法之前运行一次。这将设置静态 在运行套件之前创建图形生成器的实例,以便将其提供给随附的测试。

    未来 我希望通过使用 java 泛型可以进一步简化和删除重复代码,但我还没有达到这一点。

    【讨论】:

    • 经过进一步的工作,我开发了一个 JUnit 套件运行器和一组注释来注释抽象类是接口的契约测试。然后,运行程序找到相关类的所有合同测试。欲了解更多信息,请参阅:github.com/Claudenw/junit-contracts
    【解决方案5】:

    以下是我对如何进行合格的单元测试的一些想法:

    首先,尝试使您的实现类完全经过测试,这意味着它的所有方法都应该被 UT 覆盖。当您需要重构代码时,这样做会为您节省大量时间。对于您的情况,可能是:

    class FooTest {
        protected Foo fooImpl;
    
        @Test
        public void testGetBaz() {
        ...
        }
    
        @Test
        public void testDoBar() {
        ...
        }
    

    }

    你会发现你需要检查你的类的内部状态,这并没有错,因为 UT 应该是一种白盒测试。

    其次,所有方法都应该单独测试,不能相互依赖。在我看来,对于您上面发布的代码,它看起来不仅仅是一个功能测试或集成测试,但它也是必要的。

    第三,我认为使用 spring 或其他容器为您组装目标对象不是一个好习惯,这会使您的测试运行相对较慢,尤其是在运行大量测试时。而且你的类本质上应该是 pojo,如果你的目标对象真的很复杂,你可以在测试类的另一个方法中完成组装。

    第四,如果某个类的父接口真的很大,将测试方法分成几组是你应该做的。 Here 是更多信息。

    【讨论】:

    猜你喜欢
    • 2012-01-13
    • 2020-10-22
    • 2016-09-03
    • 2011-12-23
    • 2010-09-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-04-08
    相关资源
    最近更新 更多