如何使用Mockito模拟void方法

1192

如何使用Mockito模拟返回void类型的方法?

我实现了一个观察者模式,但由于不知道如何操作,因此无法使用Mockito进行模拟。

我尝试在互联网上找到示例,但是没有成功。

我的类看起来像这样:

public class World {

    List<Listener> listeners;

    void addListener(Listener item) {
        listeners.add(item);
    }

    void doAction(Action goal,Object obj) {
        setState("i received");
        goal.doAction(obj);
        setState("i finished");
    }

    private string state;
    //setter getter state
} 

public class WorldTest implements Listener {

    @Test public void word{
    World  w= mock(World.class);
    w.addListener(this);
    ...
    ...

    }
}

interface Listener {
    void doAction();
}

使用mock未触发该系统。

我想展示上述系统状态,并根据它们进行断言。


14
注意,模拟对象上的空方法默认不做任何操作! - Line
2
@Line,这正是我在寻找的。听你说出来后似乎很显然。但它确实强调了一个嘲讽的原则:你只需要为被模拟类的方法模拟其效果,如返回值或异常。谢谢! - allenjom
11个回答

1444

请查看Mockito API文档。正如链接中提到的(第12点),您可以使用Mockito框架中的doThrow()doAnswer()doNothing()doReturn()系列方法来模拟void方法。

例如,

Mockito.doThrow(new Exception()).when(instance).methodName();

或者如果您想将其与后续行为结合起来,

Mockito.doThrow(new Exception()).doNothing().when(instance).methodName();

假设您想要模拟下面的World类中的setter setState(String s),则以下代码使用doAnswer方法来模拟setState

World mockWorld = mock(World.class); 
doAnswer(new Answer<Void>() {
    public Void answer(InvocationOnMock invocation) {
      Object[] args = invocation.getArguments();
      System.out.println("called with arguments: " + Arrays.toString(args));
      return null;
    }
}).when(mockWorld).setState(anyString());

10
是的,我认为将参数化为Void会更好,因为它更能表达我不关心返回类型的意思。我之前并不知道这个构造,感谢您指出来。 - sateesh
2
现在doThrow是#5了(对于我来说,使用doThrow修复了消息“此处不允许'void'类型”,对于跟随者也是如此...) - rogerdpack
1
尝试在guava中模拟版本16.0.1的RateLimiter.java时,使用doNothing().when(mockLimiterReject).setRate(100)会导致调用RateLimiter的setRate方法,从而导致空指针异常,因为某些原因mutex为空,一旦mockito对其进行字节码处理,它就无法模拟我的setRate方法,而是调用了它。 :( - Dean Hiller
3
请注意,setRate()final的,因此无法进行模拟。相反,尝试创建一个能够满足需求的实例。没有必要模拟RateLimiter - dimo414
1
你如何使用 doReturn() 来模拟一个 void 方法...? - notAChance
显示剩余2条评论

137

我认为我已经找到了对这个问题更简单的答案,只需调用一个方法的真实方法(即使它具有空返回),您可以这样做:

Mockito.doCallRealMethod().when(<objectInstance>).<method>();
<objectInstance>.<method>();

或者,您可以调用该类的所有方法的真实方法来执行此操作:
<Object> <objectInstance> = mock(<Object>.class, Mockito.CALLS_REAL_METHODS);

14
这就是真正的答案。spy()方法很好用,但通常保留在你希望对象通常执行大部分操作时使用。 - biggusjimmus
1
这是什么意思?你真的在调用这些方法吗?我以前并没有真正使用过Mockito。 - obesechicken13
1
是的,模拟将调用真实方法。如果您使用@Mock,您可以指定相同的内容:@Mock(answer = Answers.CALLS_REAL_METHODS)以获得相同的结果。 - Ale
如果您正在调用抽象方法以实现运行时多态性,则请将.doCallRealMethod()替换为doNothing()。 - Tharindu Eranga

113

在@sateesh所说的内容之上,当你只是想模拟一个无返回值方法以防止测试调用它时,你可以使用Spy这种方式:

添加至@sateesh所说的內容之上,当您只需要模擬一個無返回值方法以防止測試調用它時,您可以通過以下方式使用Spy

World world = new World();
World spy = Mockito.spy(world);
Mockito.doNothing().when(spy).methodToMock();

当您想运行测试时,请确保在 spy 对象上调用测试中的方法,而不是在 world 对象上调用。例如:

当你想要运行你的测试时,确保在 spy 对象上调用 test 方法,而不是在 world 对象上调用。例如:

assertEquals(0, spy.methodToTestThatShouldReturnZero());

60
所谓问题的解决方案是使用 spy Mockito.spy(...) 而非 mock Mockito.mock(..)
Spy使我们能够进行局部模拟。Mockito在这方面很擅长。因为你有一个不完整的类,在这种方式下你可以模拟该类中某些必需的位置。

3
因为我遇到了类似的问题(同时巧合地也在测试Subject/Observer交互),所以我闯进来了。我已经在使用spy,但我希望'SubjectChanged'方法做些不同的事情。我可以使用 verify(observer).subjectChanged(subject) 来确认该方法是否被调用。然而,出于某种原因,我更愿意覆盖该方法。对此,结合Sateesh的方法和你在这里的回答是可行的方式... - gMale
37
不,这样做实际上不能帮助模拟void方法。诀窍是使用sateesh答案中列出的四个Mockito静态方法之一。 - Dawood ibn Kareem
2
@Gurnard,针对你的问题,请查看这个链接:https://dev59.com/O3NA5IYBdhLWcg3wHp-B。 - ibrahimyilmaz

48

首先:您应该始终导入mockito static,这样代码将更易读(和直观):

import static org.mockito.Mockito.*;

对于部分mocking且仍保留其余原始功能,mockito提供了“Spy”。

您可以按如下方式使用:

private World world = spy(new World());

要防止某个方法被执行,您可以使用以下代码:

doNothing().when(someObject).someMethod(anyObject());

使用 "when" 和 "thenReturn" 自定义方法的行为:

doReturn("something").when(this.world).someMethod(anyObject());

更多示例请在文档中查找优秀的Mockito样例。


32

如何使用Mockito模拟void方法 - 有两种选项:

  1. doAnswer - 如果我们希望我们的模拟void方法执行某些操作(即使是void类型也能模拟其行为)。
  2. doThrow - 然后如果您想从模拟的void方法中抛出异常,则可以使用Mockito.doThrow()

以下是如何使用它的示例(不是理想的用例,只是想说明基本用法)。

@Test
public void testUpdate() {

    doAnswer(new Answer<Void>() {

        @Override
        public Void answer(InvocationOnMock invocation) throws Throwable {
            Object[] arguments = invocation.getArguments();
            if (arguments != null && arguments.length > 1 && arguments[0] != null && arguments[1] != null) {

                Customer customer = (Customer) arguments[0];
                String email = (String) arguments[1];
                customer.setEmail(email);

            }
            return null;
        }
    }).when(daoMock).updateEmail(any(Customer.class), any(String.class));

    // calling the method under test
    Customer customer = service.changeEmail("old@test.com", "new@test.com");

    //some asserts
    assertThat(customer, is(notNullValue()));
    assertThat(customer.getEmail(), is(equalTo("new@test.com")));

}

@Test(expected = RuntimeException.class)
public void testUpdate_throwsException() {

    doThrow(RuntimeException.class).when(daoMock).updateEmail(any(Customer.class), any(String.class));

    // calling the method under test
    Customer customer = service.changeEmail("old@test.com", "new@test.com");

}
}

你可以在我的帖子《如何使用Mockito进行mock和测试void方法(附详细示例)》中找到有关如何使用Mockito进行mock和测试void方法的更多详细信息。


2
很好的例子。注意,在Java 8中,使用lambda表达式而不是匿名类可能会更好一些: 'doAnswer((Answer<Void>) invocation -> { //CODE }).when(mockInstance).add(method());' - miwe

26

在Java 8中,假设您已经静态导入org.mockito.Mockito.doAnswer,这可以变得更加简洁:

doAnswer(i -> {
  // Do stuff with i.getArguments() here
  return null;
}).when(*mock*).*method*(*methodArguments*);

return null;非常重要,如果没有它,编译器会因为找不到适当的doAnswer覆盖而失败,并出现一些相当晦涩的错误。

例如,可以使用以下代码实现一个只立即执行传递给execute()的任何RunnableExecutorService

doAnswer(i -> {
  ((Runnable) i.getArguments()[0]).run();
  return null;
}).when(executor).execute(any());

2
一行代码: Mockito.doAnswer((i) -> null).when(instance).method(any()); - Akshay Thorve
@AkshayThorve 当你实际想要使用i进行操作时,这并不起作用。 - Tim B

17

在此加入另一个答案(无意冒犯)...

如果您不想或不能使用spy的话,需要调用doAnswer方法。但是,您不一定需要自己编写Answer。有几个默认实现可供选择。特别是CallsRealMethods

实际上,看起来像这样:

doAnswer(new CallsRealMethods()).when(mock)
        .voidMethod(any(SomeParamClass.class));
或者:
doAnswer(Answers.CALLS_REAL_METHODS.get()).when(mock)
        .voidMethod(any(SomeParamClass.class));

7

我认为你的问题是由于你的测试结构。我发现在测试类中混合Mock和传统的接口实现方法很困难(就像你在这里所做的一样)。

如果你将监听器实现为Mock,然后可以验证交互。

Listener listener = mock(Listener.class);
w.addListener(listener);
world.doAction(..);
verify(listener).doAction();

这应该能让您确信“世界”正在做正确的事情。

2
如果您需要在模拟的void方法中执行一些操作,并且需要操纵发送到void方法的参数,则可以将Mockito.doAnswer与ArgumentCaptor.capture方法结合使用。
假设您有一个SpaceService,它自动装配了一个GalaxyService,该服务具有名为someServiceMethod的void方法。
您想为SpaceService中调用GalaxyService的void方法的一个方法编写测试。您的行星也是在SpaceService中生成的。所以你没有任何机会模拟它。
这是您要为其编写测试的示例SpaceService类。
class SpaceService {
    @Autowired
    private GalaxyService galaxyService;

    public Date someCoolSpaceServiceMethod() {
        // does something

        Planet planet = new World();
        galaxyService.someServiceMethod(planet); //Planet updated in this method.

        return planet.getCurrentTime();
    }
}

GalaxyService.someServiceMethod方法需要一个星球参数,该方法会执行一些操作。请参见:

GalaxyService {
    public void someServiceMethod(Planet planet) {
        //do fancy stuff here. about solar system etc.

        planet.setTime(someCalculatedTime); // the thing that we want to test.

        // some more stuff.
    }
}

你想测试这个功能。

这里有一个例子:

ArgumentCaptor<World> worldCaptor = ArgumentCaptor.forClass(World.class);
Date testDate = new Date();

Mockito.doAnswer(mocked-> {
    World capturedWorld = worldCaptor.getValue();
    world.updateTime(testDate);
    return null;
}).when(galaxyService.someServiceMethod(worldCaptor.capture());

Date result = spaceService.someCoolSpaceServiceMethod();

assertEquals(result, testDate);

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