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

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

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

Ответ 1

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

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

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

Ответ 2

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

Ответ 3

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

Ответ 4

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

При тестировании абстрактного класса вы хотите выполнить не абстрактные методы Subject Under Test (SUT), так что насмешливая структура не то, что вы хотите.

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

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

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

Ответ 5

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

Например:

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

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

Ответ 6

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

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

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

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

Слишком плохо, что только PowerMock поможет здесь далее.

Ответ 7

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

YourClass yourObject = mock(YourClass.class);

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

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

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

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

Ответ 8

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

private AbstractClassName classToTest;

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

// Test the AbstractClassName methods.

Ответ 9

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

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

Ответ 10

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

Ответ 11

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

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