Как реализовать Chain of Responsibility тестируемым способом? - PullRequest
4 голосов
/ 20 января 2012

Я хотел бы реализовать шаблон цепочки ответственности, заботясь о проблеме «неработающей связи» следующим образом:

 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. Измените видимость абстрактного метода на public, но мне не нужно менять 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);
    }
  }
}

Ответы [ 2 ]

1 голос
/ 20 января 2012

Если вы используете 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));
    }
}

Затем вы можете смоделировать его таким образом (или использовать почти любую поддельную среду, поскольку у вас нет защищенных методов):

* * 1010

В этом решении вы также можете настроить наследование Handler после Callable, а затем обернуть его поверх любого другого вызываемого объекта, который может иметь преемника, и использовать обработчик вместо исходного вызываемого объекта; это гораздо более гибко.

0 голосов
/ 20 января 2012

Я бы выбрал вариант 1. Этот поддельный подкласс достаточно прост, и вы можете поместить его в свой класс tets. В подклассах только для тестирования нет ничего плохого.

...