Как реализовать 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);
}
}
}
Похоже, достаточно общий подход. Но как это можно проверить с помощью защищенного абстрактного метода? Способы решения этой проблемы:
- Реализуйте подкласс только для тестирования
Handler
который реализует абстрактный метод. Это кажется плохим для тестового обслуживания. - Измените видимость абстрактного метода на public, но мне не нужно менять SUT, чтобы приспособить тест.
- Считайте абстрактный класс достаточно простым, чтобы не требовать модульных тестов. Мммм.
- Реализовать юнит-тесты для
handleRequest
метод на одном или нескольких конкретных подклассах. Но это не похоже на разумный способ организации тестов. - Есть ли какой-нибудь способ использовать фиктивный объект? Я пробовал 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. В подклассах только для тестирования нет ничего плохого.