减少责任和合作者进行单元测试

时间:2011-07-16 13:15:26

标签: java unit-testing anti-patterns

我有一个具有明确责任的课程 - 用所需信息“丰富”一个对象。这些信息来自各种来源(服务)。例如:

public class Enricher
{
      private CounterpartyService counterPartyService;
      private BookingEntityService bookingEntityService;
      private ExchangeRateService exchangeRateService;
      private BrokerService brokerService;
      ... 6 more services

    public EnrichedTradeRequest enrichTrade(TradeRequest request)
    {
        EnrichedTradeRequest enrichedRequest = new EnrichedRequest(request);

        // Enrich with counterparty info
        enrichedRequest = enrichCounterParty(enrichedRequest)

        // Enrich with booking entity info
        enrichedRequest = enrichBookingEntity(enrichedRequest)

        // Enrich with exchange rate info
        ...

        // Enrich with broker info
        ...

        // ....etc
        return enrichedRequest;
    }

    private EnrichedTradeRequest enrichCounterparty(EnrichedRequest enrichedRequest)
    {
        // Get info from CounterpartyService
        // ...

        return enrichedRequest;
    }

此处包含“如何”丰富请求的逻辑。例如,该类可以扩展到不同类型的交易。

我们一步到位地丰富了贸易,因为我们不希望任何部分富集的物体漂浮在周围,这没有多大意义。

这个类很难进行单元测试,因为它有很多协作者(最多可以调用12个其他服务)。我需要模拟12个服务,每个服务有3或4种不同的方法。

在这种情况下,如何减少协作者的数量,并使此代码可测试?

5 个答案:

答案 0 :(得分:2)

根本问题是你有太多的合作者。如果难以测试,那么设计可能会得到改善。

一种选择是创建一个管理协作者交互的外观服务。在你的情况下,你可能有一个外观的层次结构,因为只有一个将只需要将大量的模拟转移到另一个区域,这无法解决问题。如果可能,尝试将更有可能一起使用的服务分组到外墙中。或者,如果您总是在服务中调用相同的X方法,请将该功能放在某个方法中。如果你有一个门面,反过来调用3-4个其他外墙,每个测试只需要3-4个模拟,这是更易于管理。

最终的结果是你的'richher'只会调用一个门面服务,所以测试很容易。需要对外墙进行测试,这是可以控制的。

答案 1 :(得分:2)

我认为编写可测试代码的最佳方法是练习TDD。这有助于您编写可测试代码,因为您首先需要在编写任何生产代码之前编写测试。我建议你阅读Uncle Bob's three laws of TDD。但是下面我会给你一个第一部分的总结:

  

多年来,我开始描述测试驱动开发   三个简单规则的术语。他们是:你不被允许写   任何生产代码,除非它是一个失败的单元测试通过。您   不允许再写单元测试了   失败;和编译失败是失败。你不被允许   写下足以传递一个的生产代码   失败的单元测试。

     

您必须首先为您的功能编写单元测试   打算写。但是根据规则2,你不能写很多单位   测试。一旦单元测试代码无法编译,或者失败了   断言,你必须停止并编写生产代码。但是按照规则3你   只能编写使测试编译或生成的生产代码   通过,而不是更多。

     

如果你想到这一点,你就会意识到你根本无法写作   很多代码都没有编译和执行某些东西。   实际上,这确实是重点。我们所做的一切,无论是写作   测试,编写生产代码或重构,我们保留系统   始终执行。运行测试之间的时间是订单   秒或分钟。即使10分钟也太长了。

Guide: Writing Testable Code也非常有趣,并为您提供了编写可测试代码的大量提示。

更新

重构

当您进行测试时,您只需要进行重构,但请记住,在测试失败之前,不允许编写任何生产代码。我认为你的(可能)class has to much responsibilities最多有12个合作者。

  

提取您正在改变现有行为的类。当你工作   关于现有功能,(即添加另一个条件)提取a   阶级拉着这个责任。这将开始采取   从传统课程中分离出来,您将能够测试每个课程   孤立的块(使用依赖注入)。

有效使用旧版代码

我想指出Working Effectively with Legacy Code

  

传统代码更改算法

     

当你必须在遗留代码库中进行更改时,这是一个   您可以使用的算法。

  1.确定变更点   2.找到测试点   3.打破依赖关系   4.写测试。
  5.进行更改和重构。

模拟框架

此外,我想指出,您可以使用模拟框架(例如Mockito)来消除大量的模拟对象。我玩过这个,我最喜欢这个。

答案 2 :(得分:1)

不回到复杂的设计模式:为什么不将“丰富”方法转移到相应的服务中?这样,您可以保留在Enricher类中处理哪些富集步骤的列表,但是将实际的enrich调用委托给具有实际丰富交易知识的服务。然后可以单独测试这些服务。

应用于您的代码:

public class CounterpartyService {
    private EnrichedTradeRequest enrichCounterparty(EnrichedRequest enrichedRequest)
    {
        // Enrich trade with counterparty details..
        // ..
        return enrichedRequest;
    }
}

public class Enricher
{
    private CounterpartyService counterPartyService;
    // ... 6 more services

    public EnrichedTradeRequest enrichTrade(TradeRequest request)
    {
        EnrichedTradeRequest enrichedRequest = new EnrichedRequest(request);

        // Enrich with counterparty info
        enrichedRequest = counterPartyService.enrichCounterParty(enrichedRequest);

        // Enrich with other info
        //  ...

    }
}

答案 3 :(得分:0)

你的不良问题是最近的语言漂移,单元测试已经意味着隔离测试

稍微更传统的单元测试定义来自“测试单位是发布单位”这一短语。因此,如果您的 richher 之类的东西无法被有效地讨论,指定或传递,除了它所包含的服务之外,同样适用于它的测试。

如果这给你测试形式'以这种方式戳它,并且它按照应该的方式行事',那么单位测试警察不会来逮捕你......

代理何时有理由:异步,慢速,外部,多次实现或不稳定的服务。否则,使用真实物体的测试将更快地为您提供更好的结果。

答案 4 :(得分:0)

  1. 制作原子浓缩链。
  2. 使每个浓缩都是Guava框架的功能。
  3. 单独测试和开发每个功能
  4. 将经过测试的功能合并到生产代码中。
  5. 开始 - 空洞的充实

    final Function<EnrichedTradeRequest,EnrichedTradeRequest> ID = 
      new Function<EnrichedTradeRequest,EnrichedTradeRequest>() {
         public EnrichedTradeRequest apply(EnrichedTradeRequest arg) {
           return arg;
      }});
    

    要调用它 - 调用Y = ID.apply(X)。

    然后逐一开发,测试并将每种浓缩功能引入生产中。

    import com.google.common.base.*;
    
    private FuEnrichCounterParty = ...;
    void setFuEnrichCounterParty(...) 
    private FuEnrichBookingEntity = ...
    void setFuEnrichBookingEntity(...) 
    ...
    
    public EnrichedTradeRequest enrichTrade(TradeRequest request)
    {
        EnrichedTradeRequest enrichedRequest = new EnrichedRequest(request);
        // Enrich with counterparty info
        enrichedRequest = FuEnrichCounterParty.apply(enrichedRequest)
        // Enrich with booking entity info
        enrichedRequest = FuEnrichBookingEntity(enrichedRequest)
        // Enrich with exchange rate info
        ...
        return enrichedRequest;
    
相关问题