Использование Mockito для тестирования абстрактных классов

213

Я хотел бы проверить абстрактный класс. Конечно, я могу вручную написать макет, который наследуется от класса.

Могу ли я сделать это, используя фальшивый фреймворк (я использую Mockito) вместо того, чтобы делать макет вручную? Как?

ripper234
источник
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))
Гедиминас Римса

Ответы:

315

Следующее предложение позволяет вам тестировать абстрактные классы без создания «реального» подкласса - Mock является подклассом.

используйте 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 для заглушки абстрактных методов, и если вы заглушаете какие-либо конкретные вызовы, убедитесь, что заглушка абстрактных вызовов идет первой.
Гонен, я
2
Как я могу ввести зависимости в этот тип объекта (смоделированный абстрактный класс, вызывающий реальные методы)?
Самуил
2
Это ведет себя неожиданным образом, если у рассматриваемого класса есть инициализаторы экземпляра. Mockito пропускает инициализаторы для mocks, что означает, что переменные экземпляра, которые инициализируются inline, будут неожиданно нулевыми, что может привести к NPE.
цифровая баня
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
Да, вы не можете рассчитывать на состояние объекта вообще, только код в вызываемом методе.
Дэвид Молес
О, так что методы объекта отделены от государства, отлично.
Хеликс
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

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

При тестировании абстрактного класса вы хотите выполнить неабстрактные методы тестируемого субъекта (SUT), поэтому фальшивый фреймворк - это не то, что вам нужно.

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

Вместо этого я предлагаю определить неабстрактный подкласс вашего абстрактного класса в вашем тесте. Если это приводит к слишком большому количеству кода, это может быть признаком того, что ваш класс трудно расширить.

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

NamshubWriter
источник
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

Что действительно вызывает у меня недовольство насчет абстрактных классов, так это тот факт, что ни конструктор по умолчанию YourAbstractClass () не вызывается (отсутствует super () в mock), ни кажется, что в Mockito не существует способа инициализировать свойства mock по умолчанию (например, свойства List) с пустым ArrayList или LinkedList).

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

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

Таким образом, НЕТ способа высмеивать абстрактный класс без использования реализации реального объекта (например, определения внутреннего класса в классе модульного теста, переопределения абстрактных методов) и отслеживания реального объекта (который выполняет правильную инициализацию поля).

Жаль, что только PowerMock поможет здесь в дальнейшем.

Томас Хейсс
источник
2

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

YourClass yourObject = mock(YourClass.class);

и вызвать методы, которые вы хотите проверить, так же, как и любой другой метод.

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

Все это делает создание конкретного экземпляра YouClass и избавляет вас от необходимости предоставлять пустые реализации каждого абстрактного метода.

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

Ник Холт
источник
3
Но использование макета не будет проверять конкретные методы YourClass, или я ошибаюсь? Это не то, что я ищу.
ripper234
1
Это верно, вышеописанное не сработает, если вы хотите вызвать конкретные методы абстрактного класса.
Ричард Николс
Извините, я отредактирую немного об ожидании, которое требуется для каждого метода, который вы вызываете, а не только для абстрактных.
Ник Холт
но тогда вы все еще проверяете свой макет, а не конкретные методы.
Джонатан Клотье
2

Вы можете расширить абстрактный класс анонимным классом в своем тесте. Например (используя Junit 4):

private AbstractClassName classToTest;

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

// Test the AbstractClassName methods.
DWB
источник
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.

Самуил
источник
0

PowerMock Whitebox.invokeMethod(..)может быть полезен в этом случае.

Умный кодер
источник