使用Mockito测试抽象类

我想测试一个抽象类。当然,我可以手动编写模拟继承自类。

我可以使用mock框架(我使用Mockito)而不是手工制作我的mock吗?如何?

230278 次浏览

假设你的测试类和你的测试类在同一个包中(在不同的源根下),你可以简单地创建mock:

YourClass yourObject = mock(YourClass.class);

并调用您想要测试的方法,就像调用其他方法一样。

你需要为每个被调用的方法提供期望,以及任何具体方法调用超方法的期望——不确定如何用Mockito做到这一点,但我相信这在EasyMock中是可能的。

所有这一切都是创建YouClass的具体实例,并节省了为每个抽象方法提供空实现的工作。

顺便说一句,我经常发现在我的测试中实现抽象类很有用,它作为一个示例实现,我通过它的公共接口进行测试,尽管这确实依赖于抽象类提供的功能。

模拟框架的设计目的是为了更容易模拟出正在测试的类的依赖关系。当您使用模拟框架模拟一个类时,大多数框架都会动态地创建一个子类,并将方法实现替换为用于检测方法何时被调用并返回假值的代码。

在测试抽象类时,您希望执行Subject Under Test (SUT)的非抽象方法,因此模拟框架并不是您想要的。

造成这种困惑的部分原因是,您所链接的问题的答案要求手工创建一个从抽象类扩展而来的模拟。我不认为这样的课程是mock。mock是一个类,它被用作依赖项的替代品,它是按照期望进行编程的,并且可以查询这些期望是否得到满足。

相反,我建议在测试中定义抽象类的非抽象子类。如果这导致了太多的代码,那么这可能表明您的类难以扩展。

另一种解决方案是让你的测试用例本身是抽象的,用抽象的方法来创建SUT(换句话说,测试用例将使用模板方法设计模式)。

尝试使用自定义答案。

例如:

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

它将为抽象方法返回mock,为具体方法调用real方法。

您可以通过使用间谍来实现这一点(尽管使用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()));

真正让我对模拟抽象类感到糟糕的是,既没有调用默认构造函数YourAbstractClass()(在mock中缺少super()),也似乎在Mockito中没有任何方法来默认初始化模拟属性(例如List属性具有空ArrayListLinkedList)。

我的抽象类(基本上生成了类源代码)没有为列表元素提供依赖setter注入,也没有初始化列表元素的构造函数(我试图手动添加)。

只有类属性使用默认初始化:

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

因此,如果不使用真正的对象实现(例如单元测试类中的内部类定义,覆盖抽象方法)和监视真正的对象(它会进行适当的字段初始化),就无法模拟抽象类。

太糟糕了,只有PowerMock才能进一步帮助这里。

下面的建议让你测试抽象类,而不创建一个“真正的”子类——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());
}
}

注意:这个解决方案的美妙之处在于你不需要来实现抽象方法,只要它们从未被调用。

在我看来,这比使用间谍更简洁,因为间谍需要一个实例,这意味着您必须为抽象类创建一个可实例化的子类。

如果你只需要测试一些具体方法而不涉及任何抽象,你可以使用CALLS_REAL_METHODS(参见Morten的回答),但如果被测试的具体方法调用一些抽象,或未实现的接口方法,这将不起作用——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();
}
}

更新如下:

对于非void方法,你需要使用thenCallRealMethod()来代替,例如:

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

否则Mockito将报怨“检测到未完成的存根”。

您可以在测试中使用匿名类扩展抽象类。 例如(使用Junit 4):

private AbstractClassName classToTest;


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


// Test the AbstractClassName methods.

您可以实例化一个匿名类,注入您的模拟,然后测试该类。

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

记住,抽象类ClassUnderTest的属性myDependencyService的可见性必须为protected

PowerMock的Whitebox.invokeMethod(..)在这种情况下很方便。

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

缺点是如果需要构造函数参数,则不能使用它。

class Dependency{
public void method(){};
}


public abstract class My {


private Dependency dependency;
public abstract boolean myAbstractMethod();


public void myNonAbstractMethod() {
// ...
dependency.method();
}
}


@RunWith(MockitoJUnitRunner.class)
public class MyTest {


@InjectMocks
private My my = Mockito.mock(My.class, Mockito.CALLS_REAL_METHODS);
// we can mock dependencies also here
@Mock
private Dependency dependency;


@Test
private void shouldPass() {
// can be mock the dependency object here.
// It will be useful to test non abstract method
my.myNonAbstractMethod();
}
}