使用Mockito测试抽象类


213

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

我可以使用模拟框架(我正在使用Mockito)来执行此操作,而不是手工制作模拟吗?怎么样?


2
从Mockito 1.10.12开始,Mockito直接支持监视/模拟抽象类:SomeAbstract spy = spy(SomeAbstract.class);
pesche 2014年

6
从Mockito 2.7.14开始,您还可以通过mock(MyAbstractClass.class, withSettings().useConstructor(arg1, arg2).defaultAnswer(CALLS_REAL_METHODS))
Gediminas Rimsa '18

Answers:


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来存根抽象方法,并且如果存根任何具体的调用,请确保先存根抽象调用。
Gonen I 2014年

2
如何在此类对象(模拟的抽象类调用实际方法)中注入依赖项?
塞缪尔

2
如果所讨论的类具有实例初始化器,则此行为以意外的方式发生。Mockito会跳过用于模拟的初始化程序,这意味着内联初始化的实例变量将意外为null,这可能会导致NPE。
digitalbath

1
如果抽象类构造函数采用一个或多个参数怎么办?
SD

67

如果您只需要测试某些具体方法而无需接触任何摘要,则可以使用 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();
    }
}

更新以添加:

对于非无效方法,您将需要使用它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

模拟框架旨在简化模拟您正在测试的类的依赖关系。当您使用模拟框架模拟类时,大多数框架会动态创建一个子类,并将该方法实现替换为用于检测何时调用方法并返回假值的代码。

在测试抽象类时,您想执行被测对象(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

真正使我对模拟抽象类感到不舒服的是,既没有调用默认构造函数YourAbstractClass()(在模拟过程中缺少super()),也没有在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的属性。myDependencyServiceClassUnderTest


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.