Mockito: издевательские зависимости "Blackbox" - PullRequest
11 голосов
/ 22 декабря 2011

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

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

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

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

public class WebAdaptor {

    private Subscriber subscriber;

    public void run() {

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

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

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

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

adaptor.setSubscriber(mockSubscriber);

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

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

Заранее спасибо!

Ответы [ 5 ]

10 голосов
/ 22 декабря 2011

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

public class WebAdaptor {

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

    private Subscriber subscriber;

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

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

5 голосов
/ 22 декабря 2011

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

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

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

2 голосов
/ 22 февраля 2013

Вы могли бы использовать 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();
    }
}
2 голосов
/ 26 декабря 2011

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

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

1 голос
/ 10 января 2012

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

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

public void run() {

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

Ответ Дэвида V решает эту проблему, добавляя подписчика в конструктор. Альтернативой, которая сохраняет скрытую конструкцию подписчика, было бы создание экземпляра подписчика в конструкторе без аргументов 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.

...