【问题标题】:How to implement Chain of Responsibility in a testable fashion?如何以可测试的方式实现责任链?
【发布时间】:2012-01-20 16:54:05
【问题描述】:

我想实现一个责任链模式,处理“断开链接”问题,如下所示:

 public abstract class Handler{

   private Handler m_successor;

   public void setSuccessor(Handler successor)
   {
     m_successor = successor;
   }

   protected abstract boolean handleRequestImpl(Request request);

   public final void handleRequest(Request request)
   {
     boolean handledByThisNode = this.handleRequestImpl(request);
     if (m_successor != null && !handledByThisNode)
     {
       m_successor.handleRequest(request);
     }
   }
 }

似乎是一种足够常见的方法。但是如何用受保护的抽象方法测试呢?处理这个问题的方法似乎是:

  1. 实现Handler 的仅测试子类,它实现了抽象方法。这似乎不利于测试维护。
  2. 将抽象方法的可见性更改为公开,但我不需要更改 SUT 以适应测试。
  3. 认为抽象类足够简单,不需要单元测试。嗯。
  4. 在一个或多个具体子类上实现handleRequest 方法的单元测试。但这似乎不是一种合理的组织测试方式。
  5. 有没有办法使用模拟对象?我试过 Mockito,但似乎无法绕过受保护的可见性。

我读过[1],这种测试问题意味着设计是错误的,建议使用组合而不是继承。我现在正在尝试这个,但是这个模式的推荐实现有这个问题似乎很奇怪,但我找不到任何关于单元测试的建议。

更新: 如图所示,我已经用依赖反转替换了抽象类,现在可以使用 Mockito 轻松测试。它看起来仍然像责任链......我错过了什么吗?

// Implement a concrete class instead
public class ChainLink {

  // Successor as before, but with new class type
  private ChainLink m_successor;

  // New type, RequestHandler
  private RequestHandler m_handler;

  // Constructor, with RequestHandler injected
  public ChainLink(RequestHandler m_handler) {
    this.m_handler = m_handler;
  }

  // Setter as before, but with new class type
  public void setSuccessor(ChainLink successor) {
    m_successor = successor;
  }

  public final void handleRequest(Request request) {
    boolean handledByThisNode = m_handler.handleRequest(request);
    if (m_successor != null && !handledByThisNode) {
      m_successor.handleRequest(request);
    }
  }
}

【问题讨论】:

    标签: java unit-testing chain-of-responsibility


    【解决方案1】:

    如果你使用 PowerMock + Mockito,你可以写一个类似这样的测试:

    @RunWith(PowerMockRunner.class)
    @PrepareForTest(Tests.class)
    public class Tests {
        @Test
        public void testHandledByFirst() throws Exception {
            Request req = ...;
            Handler h1 = mock(Handler.class);
            Handler h2 = mock(Handler.class);
    
            when(h1, "setSuccessor", h2).thenCallRealMethod();
            when(h1, "handleRequestImpl", req).thenReturn(true);
    
            h1.setSuccessor(h2);
            h1.handleRequest(req);
            verify(h2, times(0)).handleRequest(req);
        }
    
        @Test
        public void testHandledBySecond() throws Exception {
            Request req = ...;
            Handler h1 = mock(Handler.class);
            Handler h2 = mock(Handler.class);
    
            when(h1, "setSuccessor", h2).thenCallRealMethod();
            when(h1, "handleRequestImpl", req).thenReturn(false);
            h1.setSuccessor(h2);
    
            h1.handleRequest(req);
            verify(h2, times(1)).handleRequest(req);
        }
    }
    

    这将验证您的第二个处理程序的方法在第一个返回 false 时被调用,并且在它返回 true 时未被调用。

    另一种选择是遵循众所周知的“优先组合优于继承”的规则,并将您的类更改为如下内容:

    public interface Callable {
        public boolean call(Request request);
    }
    
    public class Handler {
        private Callable thisCallable;
        private Callable nextCallable;
    
        public Handler(Callable thisCallable, Callable nextCallable) {
            this.thisCallable = thisCallable;
            this.nextCallable = nextCallable;
        }
    
        public boolean handle(Request request) {
            return thisCallable.call(request) 
                || (nextCallable != null && nextCallable.call(request));
        }
    }
    

    然后你可以用这种方式模拟它(或者使用几乎任何模拟框架,因为你没有受保护的方法):

    @RunWith(PowerMockRunner.class)
    @PrepareForTest(Tests.class)
    public class Tests {
        @Test
        public void testHandledByFirst() throws Exception {
            Request req = ...;
            Callable c1 = mock(Callable.class);
            Callable c2 = mock(Callable.class);
            Handler handler = new Handler(c1, c2);
    
            when(c1.call(req)).thenReturn(true);
    
            handler.handle(req);
    
            verify(c1, times(1)).call(req);
            verify(c2, times(0)).call(req);
        }
    
        @Test
        public void testHandledBySecond() throws Exception {
            Request req = ...;
            Callable c1 = mock(Callable.class);
            Callable c2 = mock(Callable.class);
            Handler handler = new Handler(c1, c2);
    
            when(c1.call(req)).thenReturn(false);
    
            handler.handle(req);
    
            verify(c1, times(1)).call(req);
            verify(c2, times(1)).call(req);
        }
    }
    

    在此解决方案中,您还可以使 Handler 在 Callable 之后继承,然后您可以将其包装在任何其他可能具有后继的可调用对象上,并使用处理程序而不是原始可调用对象;它更加灵活。

    【讨论】:

    • 所以要明确 PowerMock 可以存根抽象受保护方法?
    • 是的。我还编辑了我的答案以展示另一种方法(通过稍微不同地实现你的类),您可以避免完全处理受保护的方法。
    • 谢谢 - 我同时用类似的东西编辑我的问题。这证实了我的想法,所以我将其标记为已回答!干杯!
    【解决方案2】:

    我会选择选项 1。那个假子类很简单,你可以把它放在你的 tets 类中。仅测试子类没有任何问题。

    【讨论】:

    • 我确实想知道我是不是把事情复杂化了。然而,虽然给出的例子很简单,但我想象抽象类会随着时间的推移变得更加复杂。现在实现一个子类已经足够简单了,但是如果它进化了呢?
    • 好吧,如果抽象基类随着时间的推移变得更加复杂,那么设计可能会出现问题。检查您的代码和设计是否符合SOLID 原则。大而复杂的基类通常是违反 SOLID 原则的标志,违反这些原则通常会导致软件无法维护。当测试难以编写时,这通常也是违反 SOLID 的标志。
    • 谢谢 - 这是我的想法,我已经用另一种方法编辑了我的问题。它仍然看起来像责任链,但更倾向于组合而不是继承。我想知道为什么一开始就没有使用这种方法来描述责任链。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-02-08
    • 1970-01-01
    • 1970-01-01
    • 2014-02-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多