Використання Mockito для тестування абстрактних класів


213

Я хотів би перевірити абстрактний клас. Звичайно, я можу вручну написати макет, який успадковується від класу.

Чи можу я це зробити, використовуючи глузуючий фреймворк (я використовую Mockito) замість того, щоб вручну виготовляти макет? Як?


2
Станом на Mockito 1.10.12 Mockito безпосередньо підтримує шпигування / знущання над абстрактними класами:SomeAbstract spy = spy(SomeAbstract.class);
pesche

6
Станом на Mockito 2.7.14 ви також можете знущатися над абстрактним класифікатором, який вимагає конструкторських аргументів черезmock(MyAbstractClass.class, withSettings().useConstructor(arg1, arg2).defaultAnswer(CALLS_REAL_METHODS))
Gediminas Rimsa

Відповіді:


315

Наступна пропозиція дозволить вам протестувати абстрактні класи без створення "реального" підкласу - Макет - це підклас.

використовувати Mockito.mock(My.class, Mockito.CALLS_REAL_METHODS), а потім знущатися над будь-якими абстрактними методами, які викликаються.

Приклад:

public abstract class My {
  public Result methodUnderTest() { ... }
  protected abstract void methodIDontCareAbout();
}

public class MyTest {
    @Test
    public void shouldFailOnNullIdentifiers() {
        My my = Mockito.mock(My.class, Mockito.CALLS_REAL_METHODS);
        Assert.assertSomething(my.methodUnderTest());
    }
}

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

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


14
Як зазначалося нижче, це не працює, коли абстрактний клас викликає абстрактні методи для тестування, що часто буває.
Річард Ніколс

11
Це насправді працює, коли абстрактний клас викликає абстрактні методи. Просто використовуйте синтаксис doReturn або doNothing замість Mockito.when для того, щоб заглушити абстрактні методи, і якщо ви заглушите конкретні дзвінки, переконайтеся, що на перше місце відбувається заглушка абстрактних викликів.
Gonen I

2
Як я можу вводити залежності в такий тип об'єкта (глузуючи з абстрактного класу, що викликає реальні методи)?
Самуїл

2
Це поводиться несподівано, якщо відповідний клас має ініціалізатори екземплярів. Mockito пропускає ініціалізатори для макетів, а це означає, що змінні екземпляри, ініціалізовані в рядку, будуть несподівано нульовими, що може спричинити NPE.
digitalbath

1
Що робити, якщо конструктор абстрактного класу приймає один або кілька параметрів?
SD

68

Якщо вам потрібно просто протестувати деякі конкретні методи, не торкаючись жодної реферати, ви можете скористатися CALLS_REAL_METHODS(див . Відповідь Мортена ), але якщо конкретний метод, що тестується, викликає деякі реферати, або непроведені методи інтерфейсу, це не буде працювати - Mockito скаржиться на "Неможливо викликати реальний метод в інтерфейсі Java".

(Так, це хитра конструкція, але деякі рамки, наприклад, Tapestry 4, накладають на це силу.)

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

public abstract class MyClass {
    @SomeDependencyInjectionOrSomething
    public abstract MyDependency getDependency();

    public void myMethod() {
        MyDependency dep = getDependency();
        dep.doSomething();
    }
}

public class MyClassTest {
    @Test
    public void myMethodDoesSomethingWithDependency() {
        MyDependency theDependency = mock(MyDependency.class);

        MyClass myInstance = mock(MyClass.class);

        // can't do this with CALLS_REAL_METHODS
        when(myInstance.getDependency()).thenReturn(theDependency);

        doCallRealMethod().when(myInstance).myMethod();
        myInstance.myMethod();

        verify(theDependency, times(1)).doSomething();
    }
}

Оновлено, щоб додати:

Для недійсних методів потрібно використовувати thenCallRealMethod()замість них, наприклад:

when(myInstance.myNonVoidMethod(someArgument)).thenCallRealMethod();

Інакше Mockito поскаржиться на "Незакінчене заглушка виявлена".


9
У деяких випадках це спрацює, проте Mockito не називає конструктора базового абстрактного класу цим методом. Це може спричинити збій "реального методу" через створений несподіваний сценарій. Таким чином, цей метод також не буде працювати у всіх випадках.
Річард Ніколс

3
Так, ви взагалі не можете розраховувати на стан об'єкта, лише код у методі, що викликається.
Девід Молес

О, так об'єктні методи відокремлюються від держави, чудово.
haelix

17

Домогтися цього можна за допомогою шпигуна (хоча використовуйте останню версію Mockito 1.8+).

public abstract class MyAbstract {
  public String concrete() {
    return abstractMethod();
  }
  public abstract String abstractMethod();
}

public class MyAbstractImpl extends MyAbstract {
  public String abstractMethod() {
    return null;
  }
}

// your test code below

MyAbstractImpl abstractImpl = spy(new MyAbstractImpl());
doReturn("Blah").when(abstractImpl).abstractMethod();
assertTrue("Blah".equals(abstractImpl.concrete()));

14

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

Під час тестування абстрактного класу потрібно виконати неабразивні методи Subject Under Test (SUT), тому глузуючий фреймворк - це не те, що потрібно.

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

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

Альтернативним рішенням було б зробити ваш тестовий випадок абстрактним, з абстрактним методом створення SUT (іншими словами, у тестовому випадку буде використаний шаблон дизайну Методу шаблону).


8

Спробуйте скористатись власною відповіддю.

Наприклад:

import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

public class CustomAnswer implements Answer<Object> {

    public Object answer(InvocationOnMock invocation) throws Throwable {

        Answer<Object> answer = null;

        if (isAbstract(invocation.getMethod().getModifiers())) {

            answer = Mockito.RETURNS_DEFAULTS;

        } else {

            answer = Mockito.CALLS_REAL_METHODS;
        }

        return answer.answer(invocation);
    }
}

Він поверне макет абстрактним методам і назве реальний метод для конкретних методів.


5

Те, що насправді змушує мене глузувати з абстрактних класів, це той факт, що ані конструктор за замовчуванням YourAb абстрактClass () не викликається (відсутнє супер () у макеті), ні здається, у Mockito не існує способу ініціалізації макетних властивостей (наприклад, властивості списку з порожнім ArrayList або LinkedList).

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

Тільки в атрибутах класу використовується ініціалізація за замовчуванням: private List dep1 = new ArrayList; приватний список dep2 = новий ArrayList

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

Шкода, що тільки PowerMock допоможе тут далі.


2

Припускаючи, що ваші тестові класи знаходяться в тому ж пакеті (під іншим корінням джерела), що і ваші тестові класи, ви можете просто створити макет:

YourClass yourObject = mock(YourClass.class);

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

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

Все це робиться - це створити конкретний екземпляр YouClassта заощадити зусилля щодо забезпечення порожніх реалізацій кожного абстрактного методу.

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


3
Але використання макету не перевірятиме конкретні методи YourClass, чи я помиляюся? Це не те, чого я прагну.
ripper234

1
Це правильно, вищесказане не спрацює, якщо ви хочете використовувати конкретні методи абстрактного класу.
Річард Ніколс

Вибачте, я відредагую частину очікувань, необхідних для кожного методу, який ви називаєте не лише абстрактними.
Нік Холт

але тоді ви все ще випробовуєте свій макет, а не конкретні методи.
Jonatan Cloutier

2

Ви можете продовжити абстрактний клас анонімним класом у вашому тесті. Наприклад (за допомогою Junit 4):

private AbstractClassName classToTest;

@Before
public void preTestSetup()
{
    classToTest = new AbstractClassName() { };
}

// Test the AbstractClassName methods.

2

Mockito дозволяє висміювати абстрактні класи за допомогою @Mockанотації:

public abstract class My {

    public abstract boolean myAbstractMethod();

    public void myNonAbstractMethod() {
        // ...
    }
}

@RunWith(MockitoJUnitRunner.class)
public class MyTest {

    @Mock(answer = Answers.CALLS_REAL_METHODS)
    private My my;

    @Test
    private void shouldPass() {
        BDDMockito.given(my.myAbstractMethod()).willReturn(true);
        my.myNonAbstractMethod();
        // ...
    }
}

Недоліком є ​​те, що його не можна використовувати, якщо потрібні параметри конструктора.


0

Ви можете створити екземпляр анонімного класу, ввести свої макети і потім протестувати цей клас.

@RunWith(MockitoJUnitRunner.class)
public class ClassUnderTest_Test {

    private ClassUnderTest classUnderTest;

    @Mock
    MyDependencyService myDependencyService;

    @Before
    public void setUp() throws Exception {
        this.classUnderTest = getInstance();
    }

    private ClassUnderTest getInstance() {
        return new ClassUnderTest() {

            private ClassUnderTest init(
                    MyDependencyService myDependencyService
            ) {
                this.myDependencyService = myDependencyService;
                return this;
            }

            @Override
            protected void myMethodToTest() {
                return super.myMethodToTest();
            }
        }.init(myDependencyService);
    }
}

Майте на увазі, що видимість повинна бути protectedвластивістю myDependencyServiceабстрактного класу ClassUnderTest.


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