Mockito Як знущатися над викликом методу суперкласу


94

У деяких тестах я використовую Mockito.

У мене є такі класи:

class BaseService {  
    public void save() {...}  
}

public Childservice extends BaseService {  
    public void save(){  
        //some code  
        super.save();
    }  
}   

Я хочу знущатись лише над другим викликом ( super.save) ChildService. Перший виклик повинен викликати реальний метод. Чи є спосіб зробити це?


Чи можна це вирішити за допомогою PowerMockito?
— javaPlease42,

@ javaPlease42: Так, ви можете: stackoverflow.com/a/23884011/2049986 .
— Якоб ван Лінген,

Відповіді:


57

Ні, Mockito цього не підтримує.

Можливо, це не відповідь, яку ви шукаєте, але те, що ви бачите, є симптомом незастосування принципу дизайну:

Складайте перевагу над спадщиною

Якщо ви витягнете стратегію замість розширення супер класу, проблема зникне.

Якщо, однак, вам не дозволено змінювати код, але ви все одно повинні його протестувати, і таким незграбним чином все-таки є надія. За допомогою деяких інструментів AOP (наприклад, AspectJ) ви можете вплести код у метод суперкласу та повністю уникнути його виконання (юк). Це не працює, якщо ви використовуєте проксі-сервери, вам доведеться використовувати модифікацію байт-коду (або переплетення часу завантаження, або компіляція переплетення часу). Існують глузливі фреймворки, які також підтримують такий тип фокусів, як PowerMock та PowerMockito.

Я пропоную вам піти на рефакторинг, але якщо це не варіант, ви серйозно роздивляєтесь.


5
Я не бачу порушення LSP. У мене приблизно така ж установка, як і у OP: базовий клас DAO з методом findAll () та підклас DAO, який замінює базовий метод, викликаючи super.findAll (), а потім сортуючи результат. Підклас можна замінити у всіх контекстах, що приймають суперклас. Я не розумію вашого значення?

1
Я видалю зауваження LSP (це не додає значення відповіді).
— iwein

Так, успадкування відстій, і дурний фреймворк, з яким я застряг, розроблений з успадкуванням як єдиним варіантом.
— Шрідхар Сарнобат

Припускаючи, що ви не можете перепроектувати суперклас, ви можете витягти //some codesкод у метод, який можна перевірити окремо.
— фазмальний

1
Добре, зрозумів. Це інша проблема, ніж те, що я намагався вирішити, коли шукав це, але принаймні це непорозуміння вирішило мою власну проблему, щоб знущатися над дзвінками з базового класу (що я насправді не замінюю).
— Гійом Перро

86

Якщо у вас дійсно немає вибору для рефакторингу, ви можете знущатись / заглушати все у виклику супер методу, наприклад

    class BaseService {

        public void validate(){
            fail(" I must not be called");
        }

        public void save(){
            //Save method of super will still be called.
            validate();
        }
    }

    class ChildService extends BaseService{

        public void load(){}

        public void save(){
            super.save();
            load();
        }
    }

    @Test
    public void testSave() {
        ChildService classToTest = Mockito.spy(new ChildService());

        // Prevent/stub logic in super.save()
        Mockito.doNothing().when((BaseService)classToTest).validate();

        // When
        classToTest.save();

        // Then
        verify(classToTest).load();
    }

2
цей код насправді не перешкоджав би виклику права super.save (), тому, якщо ви багато робите в super.save (), вам доведеться запобігти всім цим дзвінкам ...
— iwein

фантастичне рішення творить для мене чудеса, коли я хочу повернути знущане значення із методу суперкласу для використання дитиною, фантастичне спасибі.
— Гурнард

14
це працює добре, якщо перевірка не прихована або метод збереження не працює безпосередньо замість того, щоб викликати інший метод. mockito не: Mockito.doNothing (). when ((BaseService) шпигун) .save (); це не буде робити нічого на базовій службі, крім збереження дочірньої служби :(
— tibi

Я не можу змусити це працювати на моєму - це також пригнічує метод дитини. BaseServiceє абстрактним, хоча я не розумію, чому це було б доречно.
— Шрідхар Сарнобат

1
@ Срідхар-Сарнобат, так, я бачу те саме :( хтось знає, як змусити його лише super.validate()
— заглушити

4

Подумайте про переробку коду з методу ChildService.save () на інший метод і протестуйте цей новий метод замість тестування ChildService.save (), таким чином ви уникнете непотрібного виклику методу super.

Приклад:

class BaseService {  
    public void save() {...}  
}

public Childservice extends BaseService {  
    public void save(){  
        newMethod();    
        super.save();
    }
    public void newMethod(){
       //some codes
    }
} 

1

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


чи можу я також сказати, що склад над успадкуванням майже завжди кращий, але іноді його просто простіше використовувати у спадок. доки Java не включатиме кращу композиційну модель, як Scala або Groovy, це буде завжди так, і ця проблема буде продовжувати існувати
— Лука

1

Навіть якщо я повністю погоджуюсь з відповіддю iwein (

надавати перевагу композиції над спадщиною

), я визнаю, що іноді успадкування здається цілком природним, і я не відчуваю його порушення чи рефакторингу лише заради юніт-тесту.

Отже, моя пропозиція:

/**
 * BaseService is now an asbtract class encapsulating 
 * some common logic callable by child implementations
 */
abstract class BaseService {  
    protected void commonSave() {
        // Put your common work here
    }

    abstract void save();
}

public ChildService extends BaseService {  
    public void save() {
        // Put your child specific work here
        // ...

        this.commonSave();
    }  
}

А потім, в модульному тесті:

    ChildService childSrv = Mockito.mock(ChildService.class, Mockito.CALLS_REAL_METHODS);

    Mockito.doAnswer(new Answer<Void>() {
        @Override
        public Boolean answer(InvocationOnMock invocation)
                throws Throwable {
            // Put your mocked behavior of BaseService.commonSave() here
            return null;
        }
    }).when(childSrv).commonSave();

    childSrv.save();

    Mockito.verify(childSrv, Mockito.times(1)).commonSave();

    // Put any other assertions to check child specific work is done

0

Причина в тому, що ваш базовий клас не є загальнодоступним, тоді Mockito не може перехопити його через видимість, якщо ви зміните базовий клас як загальнодоступний або @Override у підкласі (як загальнодоступний), тоді Mockito може правильно знущатись над ним.

public class BaseService{
  public boolean foo(){
    return true;
  }
}

public ChildService extends BaseService{
}

@Test
@Mock ChildService childService;
public void testSave() {
  Mockito.when(childService.foo()).thenReturn(false);

  // When
  assertFalse(childService.foo());
}

8
Справа не в цьому. ChildService має перевизначити foo (), і проблема полягає в тому, як знущатися над BaseService.foo (), але не ChildService.foo ()
— Адріан Костер

0

Можливо, найпростіший варіант, якщо спадкування має сенс - це створити новий метод (пакунок private ??) для виклику супер (дозволимо назвати його superFindall), шпигувати реальний екземпляр, а потім знущатися над методом superFindAll () так, як ви хотіли знущатись батьківський клас. Це не ідеальне рішення з точки зору охоплення та видимості, але воно повинно виконувати роботу, і його легко застосувати.

 public Childservice extends BaseService {
    public void save(){
        //some code
        superSave();
    }

    void superSave(){
        super.save();
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.