使用Mockito测试抽象类

rip*_*234 202 java abstract-class unit-testing mocking mockito

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

我可以使用模拟框架(我使用的是Mockito)而不是手工制作我的模拟吗?怎么样?

Mor*_*cus 305

以下建议让您在不创建"真实"子类的情况下测试抽象类--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());
    }
}
Run Code Online (Sandbox Code Playgroud)

注意:此方法的好处是,你不具备实现的抽象方法,只要他们永远不会被调用.

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

  • 如下所述,当抽象类调用抽象方法以进行测试时,这不起作用,这种情况经常发生. (14认同)
  • 当抽象类调用抽象方法时,这实际上可行.只需使用doReturn或doNothing语法而不是Mockito.when来存根抽象方法,如果你存根任何具体的调用,请确保首先对抽象调用进行存根. (10认同)
  • 如何在这种对象中注入依赖项(模拟抽象类调用实际方法)? (2认同)
  • 如果所讨论的类具有实例初始值设定项,则这会以意想不到的方式运行。Mockito 跳过模拟的初始化程序,这意味着内联初始化的实例变量将意外为空,这可能会导致 NPE。 (2认同)
  • 如果抽象类构造函数接受一个或多个参数怎么办? (2认同)

Dav*_*les 66

如果你只是需要测试一些具体的方法而不涉及任何摘要,你可以使用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();
    }
}
Run Code Online (Sandbox Code Playgroud)

更新以添加:

对于非void方法,您需要使用thenCallRealMethod(),例如:

when(myInstance.myNonVoidMethod(someArgument)).thenCallRealMethod();
Run Code Online (Sandbox Code Playgroud)

否则,Mockito会抱怨"检测到未完成的茬".

  • 这在某些情况下会起作用,但是Mockito不会使用此方法调用底层抽象类的构造函数.这可能导致"真实方法"由于创建意外情况而失败.因此,这种方法也不适用于所有情况. (7认同)
  • 是的,你根本不能指望对象的状态,只能调用方法中的代码. (2认同)

Ric*_*ols 16

您可以通过使用间谍来实现这一目标(尽管使用最新版本的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()));
Run Code Online (Sandbox Code Playgroud)


Nam*_*ter 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

它将返回抽象方法的模拟,并将为具体方法调用实际方法.


小智 6

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();
  }
}
Run Code Online (Sandbox Code Playgroud)


小智 5

真正让我对模拟抽象类感觉不好的事实是,默认构造函数YourAbstractClass()都没有被调用(模拟中缺少super()),也没有任何方式在Mockito中默认初始化模拟属性(例如List属性)使用空ArrayList或LinkedList).

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

只有类属性使用默认初始化:private List dep1 = new ArrayList; private List dep2 = new ArrayList

因此没有办法在不使用真实对象实现(例如,单元测试类中的内部类定义,重写抽象方法)和监视真实对象(进行适当的字段初始化)的情况下模拟抽象类.

太糟糕了,只有PowerMock才能进一步提供帮助.


Jor*_*tor 5

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();
        // ...
    }
}
Run Code Online (Sandbox Code Playgroud)

缺点是如果需要构造函数参数则无法使用。