2011-12-22 5 views
10

Итак, меня попросили прочитать насмешку и BDD для нашей команды разработчиков и поиграть с макетами, чтобы улучшить небольшую часть наших существующих модульных тестов (в качестве эксперимента).Mockito: Mocking «Blackbox» Зависимости

В конечном итоге я решил пойти с Mockito по ряду причин (некоторые из которых находятся вне сферы моего контроля), а именно потому, что он поддерживает как прерывание, так и насмешку над случаями, когда насмешка не подходит.

Я весь день узнал о Мокито, насмешливый (в общем) и BDD. И теперь я готов копаться и начать дополнять наши модульные тесты.

Итак, мы имеем класс под названием WebAdaptor, который имеет run() метод:

public class WebAdaptor { 

    private Subscriber subscriber; 

    public void run() { 

     subscriber = new Subscriber(); 
     subscriber.init(); 
    } 
} 

Обратите внимание: (! По причинам, выходящим за рамки этого вопроса) я не имею способ изменить этот код , Таким образом, я делаю не имеют возможность добавить метод сеттера для Subscriber, и поэтому его можно считать недостижимым «черным ящиком» внутри моего WebAdaptor.

Я хочу написать модульный тест, который включает в Mockito издеваться, и использует, что макет для verify, что выполнение WebAdaptor::run() вызывает Subscriber::init() называться.

Итак, вот что у меня до сих пор (внутри WebAdaptorUnitTest):

@Test 
public void runShouldInvokeSubscriberInit() { 

    // Given 
    Subscriber mockSubscriber = mock(Subscriber.class); 
    WebAdaptor adaptor = new WebAdaptor(); 

    // When 
    adaptor.run(); 

    // Then 
    verify(mockSubscriber).init(); 
} 

Когда я запускаю этот тест, фактический Subscriber::init() метод запускается на выполнение (я могу сказать от консольного вывода и видящие файлы генерируются в моей локальной системе), неmockSubscriber, который не должен делать (или возвращать) что угодно.

Я проверил и повторно проверены: init является public, ни static или final, и он возвращает void. Согласно документам, у Mockito не должно быть проблем, издеваясь над этим объектом.

Так это заставило меня подумать: мне нужно явно связать mockSubscriber с adaptor? Если это так, то обычно, следующее обычно это исправить:

adaptor.setSubscriber(mockSubscriber); 

Но так как я не могу добавить любой такой сеттер (пожалуйста, прочитайте мою записку выше), я в недоумении, как я мог принудить такую ​​ассоциацию. Итак, несколько очень близких вопросов:

  • Может ли кто-нибудь подтвердить, что я правильно установил тест (используя API Mockito)?
  • Является ли мое подозрение в отношении отсутствующего сеттера правильным? (Нужно ли связывать эти объекты с помощью сеттера?)
  • Если мое подозрение выше, и я не могу изменить WebAdaptor, существуют ли какие-либо ограничения в моем распоряжении?

Заранее благодарен!

+0

Это не прямо ответить на ваш вопрос, но JMockit делает этот вид черного ящика издевательский довольно легко. Является ли JMockIt вариантом для вас? –

+0

Как создается подписчик в этом классе? Можно ли переопределить код экземпляра, чтобы вернуть экземпляр, который вы контролируете? –

+0

run() - единственный метод, который использует подписчика, поэтому, во всяком случае, он должен быть локальной переменной внутри этого метода. Опять же, я не могу изменить код ... – IAmYourFaja

ответ

10

Вам нужно придать макет классу, который вы тестируете.Вам не нужен доступ к подписчику. Способ использования mockito и других издевательских фреймворков заключается в том, что вам не нужен доступ к объектам, с которыми вы взаимодействуете. Тем не менее вам нужен способ получить макет объектов в классе, который вы тестируете.

public class WebAdaptor { 

    public WebAdaptor(Subscriber subscriber) { /* Added a new constructor */ 
     this.subscriber = subscriber; 
    } 

    private Subscriber subscriber; 

    public void run() { 
     subscriber.init(); 
    } 
} 

Теперь вы можете проверить свои взаимодействия на макет, а не на реальный объект.

@Test 
public void runShouldInvokeSubscriberInit() { 

    // Given 
    Subscriber mockSubscriber = mock(Subscriber.class); 
    WebAdaptor adaptor = new WebAdaptor(mockSubscriber); // Use the new constructor 

    // When 
    adaptor.run(); 

    // Then 
    verify(mockSubscriber).init(); 
} 

При добавлении подписчика конструктору не правильный подход, вы могли бы также рассмотреть возможность использования завода, чтобы WebAdaptor инстанцировать новые объекты для подписчиков с завода, который вы контролируете. Вы могли бы затем издеваться над фабрикой, чтобы провайдер подкапывал подписчиков.

+0

В вашем фрагменте для WebAdaptor по-прежнему есть метод run(), создающий новый подписчик. FYI. –

+0

David V - услышав меня вслух (как я писал этот вопрос), в сочетании с вашим ответом и комментариями выше, похоже, что изменение кода «WebAdaptor» - это мой единственный вариант. Спасибо за ваш ответ! – IAmYourFaja

+2

Чтобы ваш класс WebAdaptor работал с существующим кодом, вы также можете иметь конструктор no-arg, который вызывает ваш новый конструктор. Тогда существующие не-тестовые методы использования класса могут использовать конструктор no-arg. Таким образом, новый конструктор будет «public WebAdaptor() {this (new Subscriber());}'. Кроме того, конструктор с аргументом Subscriber должен быть закрытым пакетом. –

5

Если вы не хотите изменять производственный код и по-прежнему можете издеваться над функциональностью класса Subscriber, вы должны взглянуть на PowerMock. Он отлично работает вместе с Mockito и позволяет вам высмеивать создание новых объектов.

Subscriber mockSubscriber = mock(Subscriber.class); 
whenNew(Subscriber.class).withNoArguments().thenReturn(mockSubscriber); 

Более подробно объясняются в рамках documentation for the PowerMock.

+0

Прочитайте [Пример №3 - Макет конструкции новых объектов] (http://blog.jayway.com/2009/10/28/untestable-code-with-mockito-and-powermock/) (прокрутите вниз) для более исчерпывающий пример. – matsev

2

Существует способ ввести ваш макет в тестируемый класс без внесения каких-либо изменений в код. Это можно сделать, используя Mockito WhiteBox. Это очень хорошая функция, которая может быть использована для ввода зависимостей вашего теста класса из ваших тестов. Ниже приведен простой пример того, как это работает,

@Mock 
Subscriber mockSubscriber; 
WebAdaptor cut = new WebAdaptor(); 

@Before 
public void setup(){ 
    //sets the internal state of the field in the class under test even if it is private 
    MockitoAnnotations.initMocks(this); 

    //Now the whitebox functionality injects the dependent object - mockSubscriber 
    //into the object which depends on it - cut 
    Whitebox.setInternalState(cut, "subscriber", mockSubscriber); 
} 

@Test 
public void runShouldInvokeSubscriberInit() { 
    cut.run(); 
    verify(mockSubscriber).init(); 
} 

Надеется, что это помогает :-)

+0

@zharvey было полезно? или я ошибся? – Bala

+2

Это не сработает. Когда cut.run() называется новым, не посмеянным, экземпляр Subscriber будет создан внутри WebAdapter и заменит измененную версию. Затем будет вызываться метод init нового экземпляра. –

+0

Да, вы правы. Я скучаю по этому. – Bala

1

Вы не можете издеваться Абонентом с использованием Mockito в текущей реализации.

У вас возникла проблема в том, что Абонент сконструирован, а затем сразу же получил доступ, Mockito не имеет возможности заменить (или шпионить) экземпляр подписчика после создания, но до вызова метода init.

public void run() { 

    subscriber = new Subscriber(); 
    // Mockito would need to jump in here 
    subscriber.init(); 
} 

Ответ Дэвида V разрешает это путем добавления подписчика к конструктору. Альтернативой, которая сохраняет скрытую конструкцию подписчика, является создание экземпляра подписчика в конструкторе no-arg WebAdapter, а затем использование рефлекса для замены этого экземпляра перед вызовом метода run.

Ваш WebAdapter будет выглядеть так,

public class WebAdaptor { 

    private Subscriber subscriber; 

    public WebAdaptor() { 
     subscriber = new Subscriber(); 
    } 

    public void run() {    
     subscriber.init(); 
    } 
} 

И вы могли бы использовать ReflectionTestUtils из тестового модуля Springframework, чтобы придать зависимости в эту частную область.

@Test 
public void runShouldInvokeSubscriberInit() { 

    // Given 
    Subscriber mockSubscriber = mock(Subscriber.class); 
    WebAdaptor adaptor = new WebAdaptor(); 
    ReflectionTestUtils.setField(adaptor "subscriber", mockSubscriber); 

    // When 
    adaptor.run(); // This will call mockSubscriber.init() 

    // Then 
    verify(mockSubscriber).init(); 
} 

ReflectionTestUtils действительно просто оберткой об отражении в Java, то же самое может быть достигнуто вручную (и гораздо более пространно) без пружины зависимости.

Mockito's WhiteBox (как предлагает Bala) будет работать здесь вместо ReflectionTestUtils, он содержится в внутренней упаковке Mockito, поэтому я уклоняюсь от него, YMMV.

2

Вы могли бы использовать PowerMock для имитации вызова конструктора без изменения исходного кода:

import org.mockito.Mockito; 
import org.powermock.api.mockito.PowerMockito; 
import org.powermock.core.classloader.annotations.PrepareForTest; 
import org.powermock.modules.junit4.PowerMockRunner; 

@RunWith(PowerMockRunner.class) 
@PrepareForTest(WebAdaptor.class) 
public class WebAdaptorTest { 
    @Test 
    public void testRunCallsSubscriberInit() { 
     final Subscriber subscriber = mock(Subscriber.class); 
     whenNew(Subscriber.class).withNoArguments().thenReturn(subscriber); 
     new WebAdaptor().run(); 
     verify(subscriber).init(); 
    } 
} 
Смежные вопросы