How can I mock a void method to throw an exception

2019-04-23 08:51发布

问题:

I have a structure like this:

public class CacheWrapper {
    private Map<Object, Object> innerMap;

    public CacheWrapper() {
        //initialize the innerMap with an instance for an in-memory cache
        //that works on external server
        //current implementation is not relevant for the problem
        innerMap = ...;
    }

    public void putInSharedMemory(Object key, Object value) {
        innerMap.put(key, value);
    }

    public Object getFromSharedMemory(Object key) {
        return innerMap.get(key);
    }
}

And my client class (you could say it looks like this):

public class SomeClient {
    //Logger here, for exception handling
    Logger log = ...;
    private CacheWrapper cacheWrapper;
    //getter and setter for cacheWrapper...

    public Entity getEntity(String param) {
        Entity someEntity = null;
        try {
            try {
                entity = cacheWrapper.getFromSharedMemory(param);
            } catch (Exception e) {
                //probably connection failure occurred here
                log.warn("There was a problem when getting from in-memory " + param + " key.", e);
            }
            if (entity == null) {
                entity = ...; //retrieve it from database
                //store in in-memory cache
                try {
                    cacheWrapper.put(param, entity);
                } catch (Exception e) {
                    //probably connection failure occurred here
                    log.warn("There was a problem when putting in in-memory " + param + " key.", e);
                }
            }
        } catch (Exception e) {
            logger.error(".......", e);
        }
        return entity;
    }
}

I'm creating unit tests for SomeClient#getEntity method and have to cover all scenarios. For instance, I need to cover the scenario where there are exceptions thrown by cacheWrapper. The approach I'm following is to create a mock for CacheWrapper class, make the methods on CacheWrapper class to throw a RuntimeException, set this mock in an instance of SomeClient and test Someclient#getEntity. The problem is when trying to mock putInSharedMemory method because is void. I have tried lot of ways to do this but none of them work. The project has dependencies for PowerMock and EasyMock.

Here are my attempts:

  1. Using EasyMock.<Void>expect. This raised a compiler error.
  2. Tried to stub CacheWrapper#putInSharedMemory. Didn't worked because raised an exception with this error message:

    java.lang.AssertionError: Unexpected method call putInSharedMemory("foo", com.company.domain.Entity@609fc98)

  3. Added Mockito dependency to the project to make use of the functionality of PowerMockito class. But this raised an exception because it doesn't integrate with EasyMock. This is the exception raised:

    java.lang.ClassCastException: org.powermock.api.easymock.internal.invocationcontrol.EasyMockMethodInvocationControl cannot be cast to org.powermock.api.mockito.internal.invocationcontrol.MockitoMethodInvocationControl

Here's the code for this unit test sample:

@Test
public void getEntityWithCacheWrapperException() {
    CacheWrapper cacheWrapper = mockThrowsException();
    SomeClient someClient = new SomeClient();
    someClient.setCacheWrapper(cacheWrapper);
    Entity entity = someClient.getEntity();
    //here.....................^
    //cacheWrapper.putInSharedMemory should throw an exception

    //start asserting here...
}

//...

public CacheWrapper mockThrowsException() {
    CacheWrapper cacheWrapper = PowerMock.createMock(CacheWrapper.class);
    //mocking getFromSharedMemory method
    //this works like a charm
    EasyMock.expect(cacheWrapper.getFromSharedMemory(EasyMock.anyObject()))
        .andThrow(new RuntimeException("This is an intentional Exception")).anyTimes();

    //mocking putInSharedMemory method
    //the pieces of code here were not executed at the same time
    //instead they were commented and choose one approach after another

    //attempt 1: compiler exception: <Void> is not applicable for <void>
    EasyMock.<Void>expect(cacheWrapper.putInSharedMemory(EasyMock.anyObject(), EasyMock.anyObject()))
        .andThrow(new RuntimeException("This is an intentional Exception")).anyTimes();

    //attempt 2: stubbing the method
    //exception when executing the test:
     //Unexpected method call putInSharedMemory("foo", com.company.domain.Entity@609fc98)
    Method method = PowerMock.method(CacheWrapper.class, "putInSharedMemory", Object.class, Object.class);
    PowerMock.stub(method).toThrow(new RuntimeException("Exception on purpose."));

    //attempt 3: added dependency to Mockito integrated to PowerMock
    //bad idea: the mock created by PowerMock.createMock() belongs to EasyMock, not to Mockito
    //so it breaks when performing the when method
    //exception:
    //java.lang.ClassCastException: org.powermock.api.easymock.internal.invocationcontrol.EasyMockMethodInvocationControl
    //cannot be cast to org.powermock.api.mockito.internal.invocationcontrol.MockitoMethodInvocationControl
    //at org.powermock.api.mockito.internal.expectation.PowerMockitoStubberImpl.when(PowerMockitoStubberImpl.java:54)
    PowerMockito.doThrow(new RuntimeException("Exception on purpose."))
        .when(cacheWrapper).putInSharedMemory(EasyMock.anyObject(), EasyMock.anyObject());

    PowerMock.replay(cacheWrapper);
    return cacheWrapper;
}

I cannot change the implementation of CacheWrapper because it comes from a third party library. Also, I cannot use EasyMock#getLastCall because I'm performing the test on SomeClient#getEntity.

How can I overcome this?

回答1:

Since none of your classes are final, you can use "pure mockito" without resorting to PowerMockito:

final CacheWrapper wrapper = Mockito.spy(new CacheWrapper());

Mockito.doThrow(something)
    .when(wrapper).putInSharedMemory(Matchers.any(), Matchers.any());

Note that "method arguments" to a stub are in fact argument matchers; you can put specific values (if not "surrounded" by a specific method it will make a call to .equals()). So, you can guide the stub's behavior differently for different arguments.

Also, no need for any kind of .replay() with Mockito, which is very nice!

Finally, be aware that you can doCallRealMethod() as well. After that, it depends on your scenarios...

(note: last mockito version available on maven is 1.10.17 FWIW)



回答2:

Are you using EasyMock or Mockito? Both are different frameworks.

PowerMockito is a superset (or more of a supplement) that can be used with both these frameworks. PowerMockito allows you to do things that Mockito or EasyMock don't.

Try this for stubbing void methods to throw exceptions:

EasyMock:

// First make the actual call to the void method.
cacheWrapper.putInSharedMemory("key", "value");
EasyMock.expectLastCall().andThrow(new RuntimeException());

Check:

  • http://easymock.org/api/org/easymock/internal/MocksControl.html#andVoid--
  • Getting EasyMock mock objects to throw Exceptions

Mockito:

 // Create a CacheWrapper spy and stub its method to throw an exception.
 // Syntax for stubbing a spy's method is different from stubbing a mock's method (check Mockito's docs).
 CacheWrapper spyCw = spy(new CacheWrapper()); 
 Mockito.doThrow(new RuntimeException())
     .when(spyCw)
     .putInSharedMemory(Matchers.any(), Matchers.any());

 SomeClient sc = new SomeClient();
 sc.setCacheWrapper(spyCw);

 // This will call spyCw#putInSharedMemory that will throw an exception.
 sc.getEntity("key");


回答3:

Use expectLastCall, like:

    cacheWrapper.putInSharedMemory(EasyMock.anyObject(), EasyMock.anyObject())
    EasyMock.expectLastCall().andThrow(new RuntimeException("This is an intentional Exception")).anyTimes();