如何模拟 void 方法来引发异常?

Posted

技术标签:

【中文标题】如何模拟 void 方法来引发异常?【英文标题】:How can I mock a void method to throw an exception? 【发布时间】:2015-03-08 02:32:11 【问题描述】:

我有这样的结构:

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

还有我的客户端类(你可以说它看起来像这样):

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;
    

我正在为SomeClient#getEntity 方法创建单元测试,并且必须涵盖所有场景。例如,我需要涵盖cacheWrapper 抛出异常的场景。我遵循的方法是为CacheWrapper 类创建一个模拟,使CacheWrapper 类上的方法抛出一个RuntimeException,在SomeClient 的实例中设置这个模拟并测试Someclient#getEntity。问题是在尝试模拟putInSharedMemory 方法时,因为是void。我已经尝试了很多方法来做到这一点,但它们都不起作用。 该项目具有 PowerMock 和 EasyMock 的依赖项

这是我的尝试:

    使用EasyMock.&lt;Void&gt;expect。这引发了编译器错误。

    试图存根CacheWrapper#putInSharedMemory。没有工作,因为引发了一个异常并显示此错误消息:

    java.lang.AssertionError: 意外的方法调用 putInSharedMemory("foo", com.company.domain.Entity@609fc98)

    向项目添加了 Mockito 依赖项,以利用 PowerMockito 类的功能。但这引发了一个异常,因为它没有与 EasyMock 集成。这是引发的异常:

    java.lang.ClassCastException:org.powermock.api.easymock.internal.invocationcontrol.EasyMockMethodInvocationControl 无法转换为 org.powermock.api.mockito.internal.invocationcontrol.MockitoMethodInvocationControl

这是这个单元测试示例的代码:

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

我无法更改 CacheWrapper 的实现,因为它来自第三方库。另外,我不能使用EasyMock#getLastCall,因为我正在对SomeClient#getEntity 执行测试。

我该如何克服这个问题?

【问题讨论】:

您为什么需要 PowerMockito?您测试的所有类都不是最终的,您可以在实例化实例上使用spy()...(您可以存根间谍)。另外,那个 .replay() 是从哪里来的? @fge 我不太擅长使用这些框架,因为我倾向于编写集成测试而不是纯单元测试。请您详细了解此spy() 方法或任何相关文档? 总而言之,测试代码真的很奇怪,你似乎同时使用了easymock和(power)mockito...有什么原因吗? @fge 添加了 powermockito 只是因为它提供了另一种解决方案,但正如尝试 3 中所述,这是一个坏主意 :) 好吧,例如,使用纯 mockito,您可以使用 final Foo foo = spy(new Foo()); doThrow(something).when(foo).someMethod(blah) 【参考方案1】:

由于您的所有课程都不是最终课程,因此您可以使用“pure mockito”而无需求助于 PowerMockito:

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

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

请注意,存根的“方法参数”实际上是参数匹配器;您可以输入特定值(如果没有被特定方法“包围”,它将调用.equals())。因此,您可以针对不同的参数以不同的方式引导存根的行为。

另外,Mockito 不需要任何类型的.replay(),这非常好!

最后,请注意您也可以doCallRealMethod()。之后,这取决于您的场景...

(注意:maven 上可用的最后一个 mockito 版本是 1.10.17 FWIW)

【讨论】:

【参考方案2】:

您使用的是 EasyMock 还是 Mockito?两者都是不同的框架。

PowerMockito 是一个超集(或更多的补充),可以与这两个框架一起使用。 PowerMockito 允许您做 Mockito 或 EasyMock 不做的事情。

尝试使用 stubing void 方法抛出异常:

EasyMock:

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

检查:

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】:

使用expectLastCall,比如:

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

【讨论】:

@LuiggiMendoza 抱歉。 这会抛出java.lang.IllegalStateException: no last call on a mock available。我正在测试SomeClient#getEntity,而不是CacheWrapper @LuiggiMendoza 好的,我理解错了;所以,你的意思是让 .getEntity() 抛出一个异常并捕获它?

以上是关于如何模拟 void 方法来引发异常?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Mockery 在模拟方法的第 N 次调用中引发异常

捕获异步 void 方法引发的异常

如何测试调用socket.send时是不是引发异常[重复]

如何引发 ActiveRecord::Rollback 异常并一起返回值?

模拟函数以引发异常以测试除块

JUnit 5:如何断言抛出异常?