最佳实践:测试接口契约合规性?

6
假设已定义了接口 for:
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类中。

有没有关于如何实现这些测试的最佳实践呢?

5个回答

2
这是一个依赖注入的典型例子。如果你使用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接口这样需要许多正确实现的情况下,是否适用呢? - soru
你可以有尽可能多的Spring配置,就像有多少实现一样。 - artbristol
不幸的是,创建所有这些配置可能比在代码中提供解决方案更容易出错。 - soru
@Soru 使用Spring Java配置(无需XML),非常容易。 - artbristol
这在使用IoC框架的项目中可能效果很好,而且由于我没有指定该项目不使用IoC,因此这似乎是一个好的有效答案。然而,在我的特定情况下,我们并没有为项目本身使用IoC,仅为测试而添加IoC似乎有点过度。 - cWarren

0
为什么不只是有一个名为InterfaceTester的东西,它从单元测试InterfaceImplATestInterfaceImplBTest等中被调用呢?
@Test
public void testSerialisation()
{
  MyObject a = new MyObject();

  ...

  serialisationTester.testSimpleRoundTrip(a);

  serialisationTester.testEdgeCases(a);

  ... 
}

你从中得到的诊断结果肯定很糟糕吧? - tddmonkey
据我所见,这似乎和任何其他 JUnit 测试没有什么区别。如果你真的需要比“序列化不适用于 MyObject”更详细的信息,那就将其拆分成较小的 @Tests。 - soru
这个测试用例的问题在于,每个子类的实现都需要更改MyObject的实现。 - cWarren

0

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

在下面的内容中:

  • [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测试的测试。运行测试套件比运行单个测试要复杂一些。

使用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套件运行器和一组注释来注释抽象类是接口的合同测试。然后,运行器会定位有关类的所有合同测试。有关更多信息,请参见:https://github.com/Claudenw/junit-contracts - cWarren

0

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

Sonar

Sonar是一个非常有用的工具,可以帮助您分析代码。您可以从Sonar前端看到应用程序如何被测试:

sonar unit test

正如您所看到的,Sonar可以向您展示代码是否已经被测试过。


如果有多个测试用例类,实现者如何知道他们已经在测试中包含了所有相关的测试?如果我后来发现我错过了一个测试用例并添加了该测试用例,实现者如何知道?如果测试在一个文件中,则它们将在下一个版本中捕获新的测试。 - cWarren
抱歉,误点了一下,我删除了之前的评论!我编辑了回复。个人而言,在编写具有逻辑的函数时,我仅测试我的应用程序的核心部分,这部分需要经过测试。还需要测试查询DB的函数,这样,如果您犯了错误,就可以立即得到响应。 - Vargan
你最后选择了哪个选项? - Vargan

-1

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

首先,尽量让你的实现类经过全面测试,这意味着所有方法都应该被 UT 覆盖。这样做可以在需要重构代码时节省大量时间。对于你的情况,可能是:

class FooTest {
    protected Foo fooImpl;

    @Test
    public void testGetBaz() {
    ...
    }

    @Test
    public void testDoBar() {
    ...
    }

}

你会发现需要检查你的类的内部状态,这对于单元测试来说并没有什么问题,因为它应该是一种白盒测试。

其次,所有方法都应该分别进行测试,不应互相依赖。在我看来,针对你上面发布的代码,它看起来像更多的是一个函数测试或集成测试,但也是必要的。

第三,我认为使用Spring或其他容器来为您组装目标对象并不是一个好的实践,这将使您的测试运行相对较慢,特别是当有大量测试需要运行时。而且你的类本质上应该是POJO,如果你的目标对象真的很复杂,可以在测试类的另一个方法中完成组装。

第四,如果某个类的父接口真的很大,则应该将测试方法划分为几个组。 这里提供了更多信息。


1
很抱歉,这并没有回答问题 - 它对接口一无所知。而且在我看来,它包含了一些明显错误的建议,比如隔离测试单个方法。 - soru
@soru 为什么将测试方法隔离开来是一个不好的建议? - Gavin Xiong
1
测试驱动开发反模式目录:“检查员”(The Inspector)。 - soru

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