关于java:使用Mockito测试抽象类

Using Mockito to test abstract classes

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

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


下面的建议让您在不创建"真实"子类的情况下测试抽象类——mock是子类。

使用Mockito.mock(My.class, Mockito.CALLS_REAL_METHODS),然后模拟调用的任何抽象方法。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
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());
    }
}

注意:这个解决方案的好处在于,只要不调用抽象方法,就不必实现它们。

老实说,这比使用间谍更整洁,因为间谍需要实例,这意味着您必须创建抽象类的可实例化子类。


如果您只需要测试一些具体的方法而不接触任何摘要,您可以使用EDCOX1×0(参见莫滕的答案),但是如果被测试的具体方法调用了一些摘要或未实现的接口方法,这将不起作用。Mockito将抱怨"不能在Java接口上调用真实方法"。

(是的,这是一个糟糕的设计,但有些框架,例如Tapestry4,有点强迫你这样做。)

解决方法是逆转这种方法——使用普通的模拟行为(即所有的模拟/存根),并使用doCallRealMethod()显式调用测试中的具体方法。例如。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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(),例如:

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

否则Mockito会抱怨"检测到未完成的存根"。


你可以通过使用间谍来实现这一点(尽管使用最新版本的mockito 1.8+)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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()));

模拟框架旨在使您测试的类的模拟依赖性更容易。当使用模拟框架模拟类时,大多数框架动态地创建一个子类,并用代码替换方法实现,以检测何时调用方法并返回假值。

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

一部分困惑是,对你链接到said-to-hand的问题的答案会制作一个从抽象类延伸出来的模拟。我不认为这样的课程是一种嘲弄。模拟是一个类,用于替换依赖项,用期望进行编程,并可以查询以查看是否满足这些期望。

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

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


尝试使用自定义答案。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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);
    }
}

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


模拟抽象类真正让我感觉不好的是,无论是调用默认构造函数yourabstractClass()(mock中缺少super()),还是在mockito中似乎都没有任何方法来默认初始化模拟属性(例如,使用空的arraylist或linkedlist列出属性)。

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

只有类属性使用默认初始化:private list dep1=new arraylist;private list dep2=新建数组列表

因此,如果不使用真实对象实现(例如单元测试类中的内部类定义、重写抽象方法)和监视真实对象(这样做可以正确初始化字段),就无法模拟抽象类。

很遗憾,只有PowerMock能在这里起到更大的作用。


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

1
2
3
4
5
6
7
8
9
private AbstractClassName classToTest;

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

// Test the AbstractClassName methods.

假设您的测试类与您的测试类位于同一个包中(在不同的源根目录下),您可以简单地创建模拟:

1
YourClass yourObject = mock(YourClass.class);

并像调用其他方法一样调用要测试的方法。

您需要为调用的每个方法提供预期,并对调用超级方法的任何具体方法进行预期-不确定如何使用Mockito实现这一点,但我相信EasyMock是可能的。

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

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


mockito允许通过@Mock注释模拟抽象类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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();
        // ...
    }
}

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


在这种情况下,invokeMethod(..)非常方便。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@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