5

次のように「リンク切れ」の問題に対処しながら、責任の連鎖パターンを実装したいと思います。

 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. handleRequest1 つ以上の具象サブクラスでメソッドの単体テストを実装します。しかし、これはテストを編成する賢明な方法とは思えません。
  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);
    }
  }
}
4

2 に答える 2

2

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 を返したときに 2 番目のハンドラーのメソッドが呼び出され、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);
    }
}

このソリューションでは、Callable の後に Handler を継承させることもできます。これにより、後続の呼び出し可能オブジェクトをラップして、元の呼び出し可能オブジェクトの代わりにハンドラーを使用することができます。それははるかに柔軟です。

于 2012-01-20T18:41:14.957 に答える
0

私はオプション 1 を選びます。その偽のサブクラスは十分に単純で、tets クラスに配置できます。テストのみのサブクラスに問題はありません。

于 2012-01-20T17:07:20.917 に答える