Как реализовать Chain of Responsibility тестируемым способом?

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

 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 ответа

Решение

Если вы используете 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, а затем обернуть его поверх любого другого вызываемого объекта, который может иметь преемника, и использовать обработчик вместо исходного вызываемого объекта; это намного более гибко.

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

Другие вопросы по тегам