Using Mockito to test abstract classes

2019-01-01 11:51发布

问题:

I\'d like to test an abstract class. Sure, I can manually write a mock that inherits from the class.

Can I do this using a mocking framework (I\'m using Mockito) instead of hand-crafting my mock? How?

回答1:

The following suggestion let\'s you test abstract classes without creating a \"real\" subclass - the Mock is the subclass.

use Mockito.mock(My.class, Mockito.CALLS_REAL_METHODS), then mock any abstract methods that are invoked.

Example:

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

Note: The beauty of this solution is that you do not have to implement the abstract methods, as long as they are never invoked.

In my honest opinion, this is neater than using a spy, since a spy requires an instance, which means you have to create an instantiatable subclass of your abstract class.



回答2:

If you just need to test some of the concrete methods without touching any of the abstracts, you can use CALLS_REAL_METHODS (see Morten\'s answer), but if the concrete method under test calls some of the abstracts, or unimplemented interface methods, this won\'t work -- Mockito will complain \"Cannot call real method on java interface.\"

(Yes, it\'s a lousy design, but some frameworks, e.g. Tapestry 4, kind of force it on you.)

The workaround is to reverse this approach -- use the ordinary mock behavior (i.e., everything\'s mocked/stubbed) and use doCallRealMethod() to explicitly call out the concrete method under test. E.g.

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

Updated to add:

For non-void methods, you\'ll need to use thenCallRealMethod() instead, e.g.:

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

Otherwise Mockito will complain \"Unfinished stubbing detected.\"



回答3:

You can achieve this by using a spy (use the latest version of Mockito 1.8+ though).

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


回答4:

Mocking frameworks are designed to make it easier to mock out dependencies of the class you are testing. When you use a mocking framework to mock a class, most frameworks dynamically create a subclass, and replace the method implementation with code for detecting when a method is called and returning a fake value.

When testing an abstract class, you want to execute the non-abstract methods of the Subject Under Test (SUT), so a mocking framework isn\'t what you want.

Part of the confusion is that the answer to the question you linked to said to hand-craft a mock that extends from your abstract class. I wouldn\'t call such a class a mock. A mock is a class that is used as a replacement for a dependency, is programmed with expectations, and can be queried to see if those expectations are met.

Instead, I suggest defining a non-abstract subclass of your abstract class in your test. If that results in too much code, than that may be a sign that your class is difficult to extend.

An alternative solution would be to make your test case itself abstract, with an abstract method for creating the SUT (in other words, the test case would use the Template Method design pattern).



回答5:

Try using a custom answer.

For example:

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

It will return the mock for abstract methods and will call the real method for concrete methods.



回答6:

What really makes me feel bad about mocking abstract classes is the fact, that neither the default constructor YourAbstractClass() gets called (missing super() in mock) nor seems there to be any way in Mockito to default initialize mock properties (e.g List properties with empty ArrayList or LinkedList).

My abstract class (basically the class source code gets generated) does NOT provide a dependency setter injection for list elements, nor a constructor where it initializes the list elements (which I tried to add manually).

Only the class attributes use default initialization: private List dep1 = new ArrayList; private List dep2 = new ArrayList

So there is NO way to mock an abstract class without using a real object implementation (e.g inner class definition in unit test class, overriding abstract methods) and spying the real object (which does proper field initialization).

Too bad that only PowerMock would help here further.



回答7:

Assuming your test classes are in the same package (under a different source root) as your classes under test you can simply create the mock:

YourClass yourObject = mock(YourClass.class);

and call the methods you want to test just as you would any other method.

You need to provide expectations for each method that is called with the expectation on any concrete methods calling the super method - not sure how you\'d do that with Mockito, but I believe it\'s possible with EasyMock.

All this is doing is creating a concrete instance of YouClass and saving you the effort of providing empty implementations of each abstract method.

As an aside, I often find it useful to implement the abstract class in my test, where it serves as an example implementation that I test via its public interface, although this does depend on the functionality provided by the abstract class.



回答8:

You can extend the abstract class with an anonymous class in your test. For example (using Junit 4):

private AbstractClassName classToTest;

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

// Test the AbstractClassName methods.


回答9:

You can instantiate an anonymous class, inject your mocks and then test that class.

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

Keep in mind that the visibility must be protected for the property myDependencyService of the abstract class ClassUnderTest.



回答10:

Whitebox.invokeMethod(..) can be handy in this case.



回答11:

Mockito allows mocking abstract classes by means of the @Mock annotation:

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

The disadvantage is that it cannot be used if you need constructor parameters.