使用Mockito测试抽象类

255

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

我能否使用一个mocking框架(我正在使用Mockito)来代替手工制作我的mock?如何操作?


6
从Mockito [1.10.12](http://site.mockito.org/mockito/docs/current/org/mockito/Mockito.html#30)开始,Mockito直接支持对抽象类进行监视/模拟:`SomeAbstract spy = spy(SomeAbstract.class);` - pesche
15
从Mockito 2.7.14开始,你也可以通过mock(MyAbstractClass.class, withSettings().useConstructor(arg1, arg2).defaultAnswer(CALLS_REAL_METHODS))来模拟需要构造函数参数的抽象类。 - Gediminas Rimsa
12个回答

373

以下建议可以让您测试抽象类,而无需创建一个“真正的”子类 - 模拟对象就是子类,只是部分模拟。

使用 Mockito.mock(My.class, Answers.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, Answers.CALLS_REAL_METHODS);
        Assert.assertSomething(my.methodUnderTest());
    }
}

注意:这个解决方案的优美之处在于您不需要实现抽象方法。 CALLS_REAL_METHODS 会使所有真实方法按原样运行,只要您不在测试中对它们进行桩操作。

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


21
如下所述,当抽象类调用抽象方法以进行测试时,这种方法不起作用,而这种情况经常发生。 - Richard Nichols
14
当抽象类调用抽象方法时,这确实有效。只需使用doReturn或doNothing语法来代替Mockito.when进行抽象方法的存根,并且如果您对任何具体调用进行存根,请确保先进行抽象调用的存根。 - Gonen I
2
我该如何在这种对象中注入依赖项(模拟抽象类调用真实方法)? - Samuel
3
如果所讨论的类拥有实例初始化器,这将表现出意外的行为。Mockito会跳过模拟对象的初始化器,这意味着在行内进行初始化的实例变量将会意外地变成null,从而导致空指针异常(NPE)。 - digitalbath
5
如果抽象类的构造函数需要一个或多个参数怎么办? - S.D.
显示剩余3条评论

77
如果您只需要测试一些具体方法而不触及任何抽象内容,可以使用CALLS_REAL_METHODS(请参见Morten's answer),但是如果要测试的具体方法调用了一些抽象或未实现的接口方法,则这种方法将无效--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会报错“检测到未完成的存根”。

9
在某些情况下这个方法可以起作用,但是Mockito在调用这个方法时并不会调用底层抽象类的构造函数。这可能会导致由于创建了意外场景而使“真实方法”失败。因此,这种方法也并非在所有情况下都适用。 - Richard Nichols
3
没错,你不能指望物体的状态,只能依靠被调用方法中的代码。 - David Moles
哦,所以对象方法与状态分离了,太好了。 - haelix

20
你可以通过使用一个监视器来实现这一点(尽管需要使用最新版本的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()));

16

Mocking框架旨在使您更轻松地模拟测试类的依赖项。当使用mocking框架模拟类时,大多数框架会动态创建一个子类,并用用于检测方法调用并返回虚假值的代码替换方法实现。

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

部分混淆是因为您链接的问题的答案建议手动制作扩展自抽象类的模拟。我不会称这样的类为mock。mock是一种用作依赖项替代品的类,编程具有期望,并且可以查询以查看是否满足这些期望。

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

另一种解决方案是将您的测试用例本身设置为抽象,具有用于创建SUT的抽象方法(换句话说,测试用例将使用Template Method设计模式)。


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

它将返回抽象方法的模拟,并对具体方法调用真实方法。

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

1
这正是我所需要的 - 一个带有@InjectMocks的抽象类测试。感谢您添加这个答案! - Andreas Dolk

5
我对嘲笑抽象类感到不满的原因是,既没有调用默认构造函数YourAbstractClass()(在模拟中缺少super()),也似乎没有任何方法在Mockito中默认初始化Mock属性(例如使用空的ArrayListLinkedList初始化List属性)。
我的抽象类(基本上是类源代码生成的)没有为列表元素提供依赖项设置注入,也没有构造函数来初始化列表元素(我试图手动添加)。
只有类属性使用默认初始化:
private List<MyGenType> dep1 = new ArrayList<MyGenType>();
private List<MyGenType> dep2 = new ArrayList<MyGenType>();

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

很遗憾,只有PowerMock可以进一步帮助解决这个问题。


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

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

// Test the AbstractClassName methods.

4

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

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

2
假设您的测试类与被测试的类位于同一包中(在不同的源代码目录下),您可以简单地创建模拟对象:
YourClass yourObject = mock(YourClass.class);

只需要像调用其他方法一样调用要测试的方法,并为每个被调用的具体方法提供期望值 - 不确定如何在Mockito中实现,但我相信使用EasyMock是可能的。

所有这些都只是创建一个YouClass的具体实例,并为您提供提供每个抽象方法的空实现所需的努力。

另外,我经常发现在我的测试中实现抽象类非常有用,因为它可以作为一个示例实现通过其公共接口进行测试,尽管这取决于抽象类提供的功能。


4
使用模拟对象无法测试 YourClass 的具体方法,这样说对吗?这不是我想要的。 - ripper234
1
没错,如果你想在抽象类上调用具体方法,上述代码是行不通的。 - Richard Nichols
抱歉,我会编辑关于期望的那一部分,这些期望不仅适用于抽象方法,而是对你调用的每个方法都是必需的。 - Nick Holt
但是这样你仍然在测试你的模拟,而不是具体的方法。 - Jonatan Cloutier

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接