关于有效企业测试的思考

哈Ha!

最近,我们回到了对测试主题的彻底研究,并且在可预见的计划中,我们甚至还拥有关于单元测试的出色书籍。同时,我们认为上下文在该主题中无处不在,因此,今天,我们提供了两个著名Java EE专家Sebastian Dashner 博客上发表的出版物(合二为一)的译文-即系列1/6和2/6,关于有效的企业测试的想法。”

企业测试是一个尚未像我们希望的那样详细地研究过的主题。编写尤其是支持测试需要花费大量时间和精力,但是,通过放弃测试来节省时间并不是一种选择。为了提高测试的有效性,应探索多少量的任务,方法和测试技术?

介绍


不管测试的类型和范围如何,准备一组测试的目的都是要确保在这种材料上,我们的应用程序在生产中将完全按预期运行。如果我们从用户的角度考虑该系统,那么这种动机应该是检查系统是否完成任务时的主要​​动机。

由于注意力范围和上下文切换是不容忽视的事情,因此我们必须确保测试不会长时间运行和测试,并且测试结果是可预测的。在编写代码时,快速验证代码(在一秒钟之内可行)至关重要-这可确保高生产率和工作重点。
另一方面,我们必须确保测试支持。软件更改非常频繁,并且在功能测试中覆盖了大量代码,因此,生产代码中的每个功能更改都需要在测试级别进行更改。理想情况下,仅当功能(即业务逻辑)发生更改时,测试代码才应更改,而清理不必要的代码并进行重构时则不应更改。通常,测试方案应包括非功能性结构更改的可能性。

当我们考虑测试的不同应用领域时,就会出现一个问题:这些领域中哪些值得花费时间和精力?例如,在微服务应用程序以及在代码分配和集成方面提供大量工作的任何系统中,集成测试尤其重要,有助于扩大系统边界。因此,我们需要一种有效的方法来在本地开发过程中对整个应用程序进行整体测试,同时以尽可能接近生产的形式维护该应用程序的环境和结构。

原则和局限性


无论选择哪种解决方案,我们都为测试套件定义以下原则和限制:

  • , . , . , , .
  • , , . , , , . , , .
  • - . , , .
  • , , . : « HTTP- gRPC, JSON - enterprise-, ..?”.
  • , , -. API, DSL .
  • « », , , , , , “dev” debug () , dev Quarkus', Telepresence, watch-and-deploy (« ») .
  • . , , , , . .
  • , , -, , , . , , , .


单元测试检查单个模块(通常是类)的行为,而所有与模块结构无关的外部因素都将被忽略或模拟。单元测试应验证各个模块的业务逻辑,而不检查其进一步的集成或配置。

根据我的经验,大多数企业开发人员对单元测试的编译方式有很好的了解。为此,您可以在我的咖啡测试项目中看到此示例。。在大多数项目中,JUnit与Mockito结合使用以模拟依赖关系,理想情况下,与AssertJ结合使用以有效地定义可读语句。我一直强调,无需特殊的扩展程序或启动程序即可执行单元测试,也就是说,可以使用常规的JUnit进行此测试。解释很简单:这全都与运行时有关,因为我们需要能够在几毫秒内运行数百个测试的功能。

通常,单元测试运行非常快,并且易于组装复杂的测试套件或特殊的工作流,因为它们易于执行,并且对测试的生命周期没有任何限制。

但是,当您有许多模拟被测类的依赖关系的单元测试时,存在一个缺点:它们与实现紧密相关(这尤其适用于类和方法),这就是为什么我们的代码难以重构的原因。换句话说,对于生产代码中的每个重构操作,还需要更改测试代码。在最坏的情况下,开发人员甚至会开始部分拒绝重构,这仅仅是因为它太繁琐,并且项目中代码的质量正在迅速下降。理想情况下,开发人员应该能够重构和重新排列元素,前提是因此,在应用程序中没有引起用户注意的更改。单元测试决不总是简化重构生产代码。

但是经验表明,单元测试对于检查用简洁的逻辑密集填充的代码或描述特定功能(例如算法)的实现的代码非常有效,并且同时不会与其他组件非常活跃地交互。特定类中的代码复杂度或密度越小,其循环复杂性越小,或者与其他组件的交互越活跃,则测试该类时单元测试的效果就越差。特别是在微服务的情况下,其中包含相对较少的业务逻辑,但提供了与外部系统的广泛集成,因此几乎不需要使用单元测试。在这样的系统中,单个模块(很少有例外)通常包含很少的专用逻辑。在决定时应考虑这一点花时间和精力更合适。

测试应用程序情况


为了解决将测试与实现紧密链接的问题,您可以尝试一种略有不同的方法来扩展测试的范围。在我的书中,我写了关于组件测试的文章,因为找不到更好的术语。但是,从本质上讲,在这种情况下,我们谈论的是测试应用情况。

应用程序状况测试是在代码级别进行的集成测试,不使用内置容器-为加快启动速度而将其废弃。他们测试了协调良好的组件的业务逻辑,这些组件通常在特定的实际案例中使用,从边界方法开始,然后一直到与之关联的所有组件。使用模拟模仿与外部系统(例如与数据库)的集成。

在不使用会自动连接组件的更先进技术的情况下构建此类方案似乎是一项艰巨的工作。但是,我们定义了可重用的测试组件,它们也是通过模拟,连接以及添加测试配置来扩展组件的测试对象。所有这些都是为了最大程度地减少重构所需的工作量。目标是创建唯一的职责,以将更改的影响程度限制在测试领域中的单个类别(或多个类别)中。为了重复使用而进行此类工作,我们减少了必要的工作总量,并且随着项目的发展,这种策略是合理的,但是每个组件只需要进行少量维修,这项工作就会迅速摊销。

为了更好地想象所有这一切,假设我们正在测试一个描述咖啡顺序的类。此类包括另外两个类别:CoffeeShopOrderProcessor



测试双打的类,CoffeeShopTestDouble并且OrderProcessorTestDouble它们*TD都位于项目,他们继承了部件的试验区CoffeeShopOrderProcessor位于该方案的主要区域。测试对象可以设置必要的仿真和连接逻辑,并有可能使用此应用程序中需要的仿真方法或验证方法来扩展类的公共接口。

下面显示了该组件的测试double类CoffeeShop

public class CoffeeShopTestDouble extends CoffeeShop {

    public CoffeeShopTestDouble(OrderProcessorTestDouble orderProcessorTestDouble) {
        entityManager = mock(EntityManager.class);
        orderProcessor = orderProcessorTestDouble;
    }

    public void verifyCreateOrder(Order order) {
        verify(entityManager).merge(order);
    }

    public void verifyProcessUnfinishedOrders() {
        verify(entityManager).createNamedQuery(Order.FIND_UNFINISHED, Order.class);
    }

    public void answerForUnfinishedOrders(List<Order> orders) {
        //     
    }
}

测试double类可以访问CoffeeShop基类的字段和构造函数以建立依赖关系。在此,也以测试双胞胎的形式使用其他组件的变体,特别 OrderProcessorTestDouble是需要它们来调用其他模拟或验证方法,这是实际情况的一部分。

测试双精度类是可重用的组件,每个组件针对每个项目的作用域编写一次,然后在许多实际情况下使用:

class CoffeeShopTest {

    private CoffeeShopTestDouble coffeeShop;
    private OrderProcessorTestDouble orderProcessor;

    @BeforeEach
    void setUp() {
        orderProcessor = new OrderProcessorTestDouble();
        coffeeShop = new CoffeeShopTestDouble(orderProcessor);
    }

    @Test
    void testCreateOrder() {
        Order order = new Order();
        coffeeShop.createOrder(order);
        coffeeShop.verifyCreateOrder(order);
    }

    @Test
    void testProcessUnfinishedOrders() {
        List<Order> orders = Arrays.asList(...);
        coffeeShop.answerForUnfinishedOrders(orders);

        coffeeShop.processUnfinishedOrders();

        coffeeShop.verifyProcessUnfinishedOrders();
        orderProcessor.verifyProcessOrders(orders);
    }

}

组件测试验证在入口点(在本例中为)调用的业务逻辑的特定情况CoffeeShop。由于所有连接和仿真都是在单独的测试对中进行的,因此此类测试简洁明了,并且以后可以使用高度专业的筛选技术,例如verifyProcessOrders()

如您所见,测试类扩展了生产类的范围,允许您安装mokee并使用验证行为的方法。尽管事实上似乎在建立这样的系统上花费了很多精力,但是如果在整个项目的框架内,我们有许多可以重用组件的实际案例,那么这些成本就会迅速摊销。我们的项目越发展,这种方法就越有用-特别是如果您注意完成测试所花费的时间。我们所有的测试用例仍然使用JUnit运行,并且在最短的时间内,它们可以成百上千次执行。

这是此方法的主要优点:组件测试的运行速度与常规单元测试一样快,但是,由于需要对单个组件或仅对几个组件进行更改,因此它们刺激了生产代码的重构。此外,通过使用针对我们主题领域的富有表现力的调优和验证方法来改进测试对象,我们提高了代码的可读性,便利了代码的使用并摆脱了测试脚本中的定型代码。

All Articles