Эквивалент ответа.RETURNS_DEEP_STUBS для шпиона в mockito

Мне не удалось найти способ использовать «глубокие заглушки» для методов заглушки шпиона в Mockito. Я хочу сделать что-то вроде этого:

@Spy private Person person = //retrieve person

    @Test
    public void testStubbed() {
        doReturn("Neil").when(person).getName().getFirstName();
        assertEquals("Neil", person.getName().getFirstName());
    }

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

Обычно при имитации вы должны использовать
@Mock(answer = Answers.RETURNS_DEEP_STUBS) для каждого имитируемого объекта. Однако у шпиона, похоже, нет ничего подобного.

Кто-нибудь когда-нибудь успешно делал глубокие насмешки с помощью шпиона?

Ошибка, которую я получаю, указана ниже:

String cannot be returned by getName()
getName() should return Name

Due to the nature of the syntax above problem might occur because of:
1. Multithreaded testing 
//I'm not doing multithreaded testing
2. A spy is stubbed using when(spy.foo()).then() syntax. It is safer to stub spies with doReturn|Throw() family of methods 
//As shown above, I'm already using the doReturn family of methods. 

person Jared Burgett    schedule 14.12.2017    source источник


Ответы (3)


Хотя я все еще хотел бы знать, есть ли лучший способ сделать это, я хотел бы опубликовать решение для всех, кто ищет.

Приведенное ниже решение отлично работает, требуя от вас создания нового макета (или даже реального объекта/шпиона) для каждого уровня ваших зависимостей. Другими словами, вместо того, чтобы связывать вызовы методов для создания заглушки, вы имитируете каждый уровень по отдельности.

@Spy private Person person = //retrieve person
@Mock private Name name;

@Test
public void testStubbed() {
    doReturn(name).when(person).getName();
    doReturn("Neil").when(name).getName();
    assertEquals("Neil", person.getName().getFirstName());
}
person Jared Burgett    schedule 15.12.2017
comment
Вы должны удалить = new Name() из своего @Mock Name name; Mockito все равно перезапишет это значение при инициализации. - person Jeff Bowman; 18.12.2017
comment
Вы правы, обновился. Когда писал это, не обращал особого внимания на объявление переменной. - person Jared Burgett; 18.12.2017

Вы можете немного приблизиться к желаемым глубинным заглушкам, используя doAnswer(RETURNS_DEEP_STUBS), но вы не можете переопределить произвольно глубокие вызовы методов, не позаботившись о том, чтобы заглушить их родительские вызовы. Я бы придерживался ручных одноуровневых насмешек, как вы делаете в своем ответе, или использовал еще меньше насмешек, если это возможно.


Поведение шпиона по умолчанию заключается в делегировании вызова его реального метода, который обычно возвращает реальный объект (например, ваше имя), а не шпиона Mockito. Это означает, что вы обычно не сможете изменить поведение этих объектов с помощью Mockito: шпион на самом деле не тот же класс, что и объект, за которым ведется слежка, а скорее является сгенерированным подклассом, в котором каждое значение поля копируется из шпионского -по стоимости. (Копирование — важная функция, потому что делегирующий шпион будет вести себя очень неинтуитивно в отношении this, в том числе для вызовов методов и значений полей.)

Foo foo = new Foo();
foo.intValue = 42;
foo.someObject= new SomeObject();

Foo fooSpy = Mockito.spy(foo);
// Now fooSpy.intValue is 42, fooSpy.someObject refers to the exact same
// SomeObject instance, and all of fooSpy's non-final methods are overridden to
// delegate to Mockito's behavior. Importantly, SomeObject is not a spy, and
// Mockito cannot override its behavior!

Так что это не сработает:

doReturn("Neil").when(person).getName().getFirstName();
//   Mockito thinks this call ^^^^^^^^^ should return "Neil".

И это тоже не будет:

doReturn("Neil").when(person.getName()).getFirstName();
//    The object here ^^^^^^^^^^^^^^^^ won't be a mock, and even if Mockito
//    could automatically make it a mock, it's not clear whether that
//    should be the same spy instance every time or a new one every time.

В вашей ситуации я бы выбрал следующее, в порядке от наиболее предпочтительного к наименее:

  1. Создайте объект Real Name и установите его с помощью doReturn. В конце концов, похоже, что Name — это объект данных (также известный как объект-значение), что, вероятно, означает, что у него нет зависимостей, стабильное поведение и сложные для имитации переходы состояний. Вы можете ничего не получить, насмехаясь над ним.

  2. Создайте фиктивное имя и установите его как в своем ответе. Это особенно полезно, если Name сложнее, чем кажется, или если оно вообще не существует.

  3. Замените getName, чтобы вернуть глубокую заглушку...

    doAnswer(RETURNS_DEEP_STUBS).when(person).getName();
    

    ... который вы затем можете переопределить...

    doReturn("Neil").when(person.getName()).getFirstName();
    

    ...даже для произвольно глубоких значений.

    doReturn("Gaelic").when(person.getName()
                                  .getEtymology()
                                  .getFirstNameEtymology())
        .getOrigin();
    

В качестве заключительной редакционной статьи скажу, что одна из опасностей частичной имитации заключается в том, что из-за нее очень сложно определить, какое поведение является реальным, а какое подделкой; из-за этого вам может быть сложно гарантировать, что поведение, которое вы тестируете, является продукцией, а не фиктивным поведением. Еще одна опасность глубокой заглушки заключается в том, что вы можете нарушить Закон Деметры по определению. Если вы обнаружите, что часто используете этот метод в тестах, возможно, пришло время подумать о перестройке тестируемой системы.

person Jeff Bowman    schedule 18.12.2017

person    schedule
comment
Можете ли вы немного уточнить этот ответ? Не похоже, что есть что-то, связанное с исходным вопросом. - person stdunbar; 06.10.2020