Mockito: Издевательская зависимость "Blackbox"

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

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

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

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

public class WebAdaptor {

    private Subscriber subscriber;

    public void run() {

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

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

Я хочу написать unit test, который включает в себя Mockito mock, и использует этот макет для 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 is public, не является ни static, либо final, и возвращает void. Согласно документам, у Mockito не должно быть проблем, издеваясь над этим объектом.

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

adaptor.setSubscriber(mockSubscriber);

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

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

Спасибо заранее!

Ответ 1

Вам нужно ввести макет в класс, который вы тестируете. Вам не нужен доступ к подписчику. Способ использования 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();
}

Если добавление подписчика к конструктору неверно, вы также можете использовать factory, чтобы позволить WebAdaptor создавать новые объекты подписчика из factory, которые вы контролируете. Затем вы можете издеваться над подписчиками factory для провайдера.

Ответ 2

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

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

Более подробная информация приведена в документации для платформы PowerMock.

Ответ 3

Существует способ вставить ваш макет в тестируемый класс без внесения каких-либо изменений в код. Это можно сделать, используя 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();
}

Надеюсь, что это поможет: -)

Ответ 4

Вы могли бы использовать 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();
    }
}

Ответ 5

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

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

public void run() {

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

Ответ David 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, то же самое можно было бы сделать вручную (и гораздо более подробно) без зависимости Spring.

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