2016-03-10 3 views
0

Я работаю над классом, который хранит некоторую информацию о сеансе связи для взаимодействия с сторонними API. Таким образом, в основном у него много поведения и небольшие состояния для поддержания. Вот один из его методы:Дилемма тестирования модулей

public LineItem getLineItem(
     String networkId, String lineItemId) throws ApiException_Exception { 
    LineItem lineItem = null; 
    session.setCode(networkId); 
    LineItemServiceInterface lineItemService = servicesInterface.lineItemService(session); 
    StatementBuilder statementBuilder = 
     new StatementBuilder() 
      .where("id = " + lineItemId.trim()) 
      .orderBy("id ASC") 
      .limit(StatementBuilder.SUGGESTED_PAGE_LIMIT); 
    LineItemPage lineItemPage = 
     lineItemService.getLineItemsByStatement(statementBuilder.toStatement()); 
    if (lineItemPage != null && lineItemPage.getResults() != null) { 
     lineItem = lineItemPage.getResults().get(0); 
    } 
    return lineItem; 
    } 

я застрял на том, как проверить этот метод, он имеет слишком много неявных зависимостей на объекты третьих лиц. Объекты трудно создавать самостоятельно. Другой большой проблемой является то, что getLineItemByStatement выполняет сетевой вызов (SOAP) за сценой.

На моей стороне я пытаюсь высмеять внешнюю службу и проверить, запрашивает ли служба данные с правильным Statement, кроме того, что я ничего не могу сделать, поскольку у меня нет изменения состояния в моем объекте, и большая часть взаимодействие объекта - это третья сторона.

Вопрос

Большая путаница в этих сценарии того, насколько мой класс должен знать о коллаборационистов? Насколько мой тест должен знать об использовании объектов моим тестируемым методом?

пример:

@Test 
    public void shouldGetLineItem() throws ApiException_Exception { 
    when(servicesInterface.lineItemService(dfpSession)).thenReturn(mockLineItemService); 
    dfpClient.getLineItem("123", "123"); 
    Statement mockStatement = mock(Statement.class); 
    Statement statement = 
     new StatementBuilder() 
      .where("id = 123") 
      .orderBy("id ASC") 
      .limit(StatementBuilder.SUGGESTED_PAGE_LIMIT) 
      .toStatement(); 

    verify(dfpSession).setNetworkCode("123"); 
    verify(mockLineItemService).getLineItemsByStatement(isA(Statement.class)); 
    } 

Как мы можем видеть, что мой тест слишком много знает о моем методе испытуемым.

Update 1

Через некоторое время я вижу, что его стало слишком трудно единицы проверить мои классы, потому что Другой крупный в LineItem разбросана повсюду и так LineItem имеет много глубокие ссылки на другие объекты и трудно создать мой поэтому я решил создать модель домена, которая содержит соответствующие данные для моего приложения.

public LineItemDescription getLineItem(String networkId, String lineItemId) 
     throws ApiException_Exception { 
    dfpSession.setNetworkCode(networkId); 
    LineItemServiceInterface lineItemService = servicesInterface.lineItemService(dfpSession); 
    return buildLineItemDescription(
     getFirstItemFromPage(lineItemService.getLineItemsByStatement(buildStatement(lineItemId)))); 
    } 
+0

Отказывание выглядит правильным ответом на меня. Какова конкретная проблема? –

+0

@JBNizet обновлен с вопросом и модульным тестом в настоящее время. – vivek

ответ

2

Основной подход

Это похоже на случай, когда я considere модульного тестирования ограниченного значения. Кажется, что вы действительно хотите, может быть, тест, который гарантирует, что SOAP-услуга будет вызвана правильно, и результаты будут преобразованы по мере необходимости. Поэтому я бы пошел на интеграционный тест. Тест вызовет/SOAP-сервис, но я бы издевался над ним. То есть вы устанавливаете службу, в которой вы можете указать, как она будет реагировать на ваш запрос. Затем вы вызываете метод и проверяете результат.

Другие вещи, которые следует учитывать Я предполагаю, что вы уже протестировали все материалы, используемые в этом методе, используя модульный тест.

Одна вещь, которая вводит в заблуждение читателя кода и который может затруднить тестирование, тогда это должно быть несколько странное обращение с networkid. Он устанавливается в session как «код», который сам по себе странный, но он не используется. Ну, на самом деле я предполагаю, что что-то подхватывает это значение из сеанса, но это в основном глобальное состояние и затрудняет рассуждение о том, что происходит. Если вам нужно это в глобальном состоянии, чтобы не пропускать его по всему месту, переместите эту часть в отдельный метод (или извлеките остальное в новом методе), чтобы вы могли протестировать все остальное без изменения глобального состояния , Или передайте это объяснительно методам, которые на самом деле нуждаются в этом.

+0

После установки кода в следующей строке 'session' передается объект, чтобы получить службу' lineItem', поэтому в основном функция 'lineItem' зависит от сеанса. С тех пор как разработан API сторонних разработчиков, я не могу много сделать для его реорганизации. – vivek

1

Я хотел бы начать с рефакторинга метод (только путем извлечения частных методов и перемещения вещей вокруг), чтобы выглядеть примерно так:

public LineItem getLineItem(String networkId, String lineItemId) throws ApiException_Exception { 
    LineItemServiceInterface lineItemService = getLineItemServiceForNetwork(networkId); 
    return getFirstItemFromPage(lineItemService.getLineItemsByStatement(buildStatement(lineItemId))); 
} 

Глядя на эту версию кода, мы видим, что этот метод имеет в по меньшей мере, слишком много обязанностей. Например, создание и настройка LineItemServiceInterface должно быть выгружено сотруднику, который может быть изделен, или, может быть, он должен быть предоставлен вызывающим абонентом вместо networkId (поскольку, если вызывающий абонент не предоставляет услугу, которую вы должны издеваться над сервисом поставщик-поставщик, чтобы вернуть еще один макет). Если слишком сложно (из-за множества устаревших зависимостей) разгрузить создание LineItemServiceInterface в другой класс, быстрой и грязной альтернативой было бы сделать getLineItemServiceInterface() защищенным или уровнем пакета и переопределить его, чтобы вернуть макет в подкласс, который вы используете для теста.

Таким образом, для случая «нормальное использование» ваш тест этого метода необходимо 1) корешок (с использованием Mockito.when()), что, когда издевались интерфейс службы получает правильно сформированный оператор с данной lineItemId, он возвращает список, содержащий один LineItem экземпляр а затем 2) убедитесь, что вопрос LineItem, о котором идет речь, возвращается getLineItem(). Тогда вы знаете, что getLineItem() правильно вызывает службу и правильно извлекает результат.

Кстати, вам не нужно высмеивать Statement. Вам нужно написать matcher, который проверяет, что экземпляр Statement, переданный в getLineItemsByStatement(), правильно сформулирован с правильным значением, порядком и лимитом идентификатора. Если Statement является сторонним классом, который не разрешает доступ к такой информации (либо напрямую через геттеры, либо опосредованно через сгенерированный код запроса), вы можете подумать о разгрузке создания Statement другому инъецированному соавтору, который вы бы издевались над этим тестом, и то вы проверяете, что сотрудник в другом месте использует тест интеграции с реальной услугой.

EDIT: на основе комментариев, вот грубый пример теста писать, предполагая дальнейший рефакторинг, чтобы разгрузить LineItemServiceInterface настроить соавтор:

@Test 
    public void shouldGetLineItem() throws ApiException_Exception { 
    when(lineItemserviceProviderMock.getLineItemService(NETWORK_ID, dfpSession)).thenReturn(mockLineItemService);  
    when(mockLineItemService.getLineItemsByStatement(argThat(statementMatcher)).thenReturn(LIST_WITH_EXPECTED_LINE_ITEM); 
    LineItem expectedResult = dfpClient.getLineItem(NETWORK_ID, LINE_ITEM_ID); 
    assertEquals(EXPECTED_LINE_ITEM, expectedResult); 
    } 

в тесте Переменная statementMatcher будет выглядеть примерно так:

ArgumentMatcher<Statement> statementMatcher = new ArgumentMatcher<Statement>{ 
     public boolean matches(Object stmt) { 
      return queryMatches(((Statement)stmt).getQuery()) && valuesMatch(((Statement)stmt).getValues()); 
     } 

     private boolean queryMatches(String query) { 
     return EXPECTED_QUERY.equals(query); 
     } 

     private boolean valuesMatch(String_ValueMapEntry[] values) { 
     // TODO: verify values here 
     } 
    } 
+0

'servicesInterface' делает именно то, что вы говорите. Я инкапсулировал первоначальное создание службы внутри моего класса соавторов, также вы можете видеть в своем тесте, что я тоже издевался над этим сервисом, поэтому мы находимся на одной странице по этой проблеме. Во-вторых, я согласен с тем, что мы можем делать частные методы, но как мы их тестируем? или как заглушить их возвращаемое значение? – vivek

+0

Также 'seeion' является обязательным аргументом для' lineItemService', который вы только что дали ему 'networkId'. Я был бы более полезен, если бы вы предоставили образец теста для вашего вышеуказанного метода. – vivek

+0

О 'session', он не передается в метод' getLineItem() ', поэтому я не передал его в' getLineItemServiceForNetwork() '. Это не значит, что он не используется, но вам не нужно знать об этом в 'getLineItem()', который не должен отвечать за настройку '' lineItemService'. –

Смежные вопросы