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

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

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


person ripper234    schedule 06.07.2009    source источник
comment
Начиная с Mockito 1.10.12, Mockito напрямую поддерживает слежку / издевательство над абстрактными классами: SomeAbstract spy = spy(SomeAbstract.class);   -  person pesche    schedule 30.12.2014
comment
Начиная с Mockito 2.7.14, вы также можете имитировать абстрактные классы, требующие аргументов конструктора, через mock(MyAbstractClass.class, withSettings().useConstructor(arg1, arg2).defaultAnswer(CALLS_REAL_METHODS))   -  person Gediminas Rimsa    schedule 13.07.2018


Ответы (11)


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

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

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

person Morten Lauritsen Khodabocus    schedule 30.11.2010
comment
+1 за помощь в решении моей проблемы, не связанной с вопросом. CALLS_REAL_METHODS был полезен для внедрения объекта-заглушки в мою SUT. Заглушка имела больше смысла, чем имитация, поскольку возвращаемые значения были немного сложными. - person Snekse; 08.12.2011
comment
Как отмечено ниже, это не работает, когда абстрактный класс вызывает абстрактные методы для тестирования, что часто бывает. - person Richard Nichols; 06.08.2012
comment
Это действительно работает, когда абстрактный класс вызывает абстрактные методы. Просто используйте синтаксис doReturn или doNothing вместо Mockito.when для заглушки абстрактных методов, а если вы заглушаете какие-либо конкретные вызовы, убедитесь, что заглушка абстрактных вызовов идет в первую очередь. - person Gonen I; 06.09.2014
comment
Как я могу внедрить зависимости в этот вид объекта (имитирующий абстрактный класс, вызывающий реальные методы)? - person Samuel; 27.07.2017
comment
Это ведет себя неожиданным образом, если у рассматриваемого класса есть инициализаторы экземпляра. Mockito пропускает инициализаторы для моков, что означает, что переменные экземпляра, которые инициализируются встроенно, будут неожиданно иметь значение NULL, что может вызвать NPE. - person digitalbath; 07.09.2017
comment
Что такое Mockito.CALLS_REAL_METHODS? - person IgorGanapolsky; 03.08.2018
comment
Что, если конструктор абстрактного класса принимает один или несколько параметров? - person S.D.; 14.03.2019

Если вам просто нужно протестировать некоторые из конкретных методов, не касаясь каких-либо рефератов, вы можете использовать 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 сообщит: «Обнаружены незавершенные заглушки».

person David Moles    schedule 12.09.2011
comment
В некоторых случаях это будет работать, однако Mockito не вызывает конструктор базового абстрактного класса с помощью этого метода. Это может привести к сбою настоящего метода из-за создания непредвиденного сценария. Таким образом, и этот метод подойдет не во всех случаях. - person Richard Nichols; 06.08.2012
comment
Да, вы вообще не можете рассчитывать на состояние объекта, только на код вызываемого метода. - person David Moles; 13.11.2012
comment
Итак, методы объекта отделены от состояния, отлично. - person haelix; 25.11.2018

Вы можете добиться этого с помощью шпиона (хотя используйте последнюю версию 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()));
person Richard Nichols    schedule 28.07.2009

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

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

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

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

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

person NamshubWriter    schedule 06.07.2009

Попробуйте использовать собственный ответ.

Например:

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);
    }
}

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

person Community    schedule 15.07.2009

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

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

Только атрибуты класса используют инициализацию по умолчанию:

private List<MyGenType> dep1 = new ArrayList<MyGenType>();
private List<MyGenType> dep2 = new ArrayList<MyGenType>();

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

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

person Thomas Heiss    schedule 02.11.2010

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

YourClass yourObject = mock(YourClass.class);

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

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

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

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

person Nick Holt    schedule 06.07.2009
comment
Но использование макета не будет проверять конкретные методы YourClass, или я ошибаюсь? Это не то, что я ищу. - person ripper234; 06.07.2009
comment
Это правильно, приведенное выше не сработает, если вы хотите вызвать конкретные методы абстрактного класса. - person Richard Nichols; 28.07.2009
comment
Извините, я отредактирую бит об ожидании, который требуется для каждого вызываемого вами метода, а не только для абстрактных. - person Nick Holt; 28.07.2009
comment
но тогда вы все еще тестируете свой макет, а не конкретные методы. - person Jonatan Cloutier; 20.01.2013

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

private AbstractClassName classToTest;

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

// Test the AbstractClassName methods.
person DwB    schedule 13.03.2015

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();
        // ...
    }
}

Недостатком является то, что его нельзя использовать, если вам нужны параметры конструктора.

person Jorge Pastor    schedule 15.08.2018

Вы можете создать экземпляр анонимного класса, внедрить свои макеты, а затем протестировать этот класс.

@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.

person Samuel    schedule 27.07.2017

В этом случае может пригодиться Whitebox.invokeMethod(..) PowerMock.

person Smart Coder    schedule 27.07.2018