You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SpyBean的void方法存根失败问题排查

问题描述

组件Foo中定义了带@Async注解的void方法bar,代码如下:

@Component
@EnableAsync
@AllArgsConstructor
public class Foo {
    private final SomeOtherService someOtherService;
    private final KafkaListenerEndpointRegistry registry;
    private final FooConfig fooConfig;

    @Async("myExecutor")
    public void bar(A a, B b, ConcurrentSkipListMap<Long, String> c, int d, int e) {
        someOtherService.doSomething(a, b, c, d, e);
    }
}

该组件被MyService调用:

@Service
@AllArgsConstructor
public class MyService {
    private final Foo foo;
        
    public void execute() {
        // call foo.bar();
    }
}

编写单元测试模拟foo.bar()抛出RuntimeException的场景,测试代码如下:

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;

import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer;

import java.util.concurrent.ConcurrentSkipListMap;

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {SpringBootMain.class},
            initializers = {ConfigDataApplicationContextInitializer.class})
@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"})
public class MyServiceTest {

    @MockBean
    private SomeOtherService someOtherService;

    @SpyBean
    @Autowired
    private Foo foo;

    @Test
    public void test() {
        MyService myService = new MyService(foo);
        Mockito.doThrow(new RuntimeException("failed")).when(foo).bar(any(A.class), any(B.class), any(ConcurrentSkipListMap.class), anyInt(), anyInt());
        myService.execute();
        // some assertions
    }
}

运行测试时出现以下错误:

WARN org.springframework.test.context.TestContextManager -- 
Caught exception while invoking 'afterTestMethod' callback on TestExecutionListener [org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener] 
for test method [public void MyServiceTest.test() throws com.fasterxml.jackson.core.JsonProcessingException] 
and test instance [MyServiceTest@60fc4450]
org.mockito.exceptions.misusing.UnfinishedStubbingException: Unfinished stubbing detected

E.g. thenReturn() may be missing.
Examples of correct stubbing:
    when(mock.isOk()).thenReturn(true);
    when(mock.isOk()).thenThrow(exception);
    doThrow(exception).when(mock).someVoidMethod();
Hints:
 1. missing thenReturn()
 2. you are trying to stub a final method, which is not supported
 3. you are stubbing the behaviour of another mock inside before 'thenReturn' instruction is completed

org.mockito.exceptions.misusing.InvalidUseOfMatchersException: 
Invalid use of argument matchers!
1 matchers expected, 5 recorded:

This exception may occur if matchers are combined with raw values:
    //incorrect:
    someMethod(any(), "raw String");
When using matchers, all arguments have to be provided by matchers.
For example:
    //correct:
    someMethod(any(), eq("String by matcher"));

For more info see javadoc for Matchers class.

尝试过以下写法,但都无法解决问题:

Mockito.doThrow(new RuntimeException("failed")).when(foo).bar(any(A.class), any(B.class), Mockito.<ConcurrentSkipListMap<Long, String>>any(), anyInt(), anyInt());
Mockito.doAnswer(inv -> {
    throw new RuntimeException("failed");
}).when(foo).bar(Mockito.any(A.class), 
                 Mockito.any(B.class), 
                 Mockito.any(ConcurrentSkipListMap.class), 
                 Mockito.anyInt(), 
                 Mockito.anyInt());
解决方案

问题根源

  1. 异步代理干扰:@Async注解会让Spring为Foo生成动态代理对象,直接对代理对象进行Stub操作时,Mockito无法正确识别原始方法的调用逻辑,导致Stub失败。
  2. 参数匹配器导入问题:如果没有静态导入ArgumentMatchers的方法,可能会引发参数匹配器的使用错误。

方案一:Stub底层依赖方法(推荐)

既然bar方法内部实际调用的是someOtherService.doSomething(),直接Stub这个底层方法抛出异常即可,无需处理异步代理的问题,更贴近业务逻辑:

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer;

import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static org.mockito.ArgumentMatchers.*;

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {SpringBootMain.class},
            initializers = {ConfigDataApplicationContextInitializer.class})
@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"})
public class MyServiceTest {

    @MockBean
    private SomeOtherService someOtherService;

    @SpyBean
    private Foo foo;

    @Test
    public void testBarThrowsException() throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(1);

        // Stub底层方法抛出异常,同时用CountDownLatch等待异步任务完成
        Mockito.doAnswer(invocation -> {
            try {
                throw new RuntimeException("failed");
            } finally {
                latch.countDown();
            }
        }).when(someOtherService)
          .doSomething(any(A.class), any(B.class), any(ConcurrentSkipListMap.class), anyInt(), anyInt());

        MyService myService = new MyService(foo);
        myService.execute();
        
        // 等待异步任务执行完成,超时时间5秒
        latch.await(5, TimeUnit.SECONDS);

        // 验证底层方法是否被调用
        Mockito.verify(someOtherService, Mockito.times(1))
               .doSomething(any(A.class), any(B.class), any(ConcurrentSkipListMap.class), anyInt(), anyInt());
    }
}

方案二:绕过异步代理Stub原始对象

如果必须直接StubFoo的bar方法,可以通过AopProxyUtils获取Foo的原始对象,再对其进行Stub操作:

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer;

import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static org.mockito.ArgumentMatchers.*;

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {SpringBootMain.class},
            initializers = {ConfigDataApplicationContextInitializer.class})
@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"})
public class MyServiceTest {

    @MockBean
    private SomeOtherService someOtherService;

    @SpyBean
    private Foo foo;

    @Test
    public void testBarThrowsException() throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(1);
        
        // 获取Foo的原始对象,绕过Spring异步代理
        Foo originalFoo = (Foo) AopProxyUtils.getSingletonTarget(foo);

        // Stub原始对象的bar方法
        Mockito.doAnswer(invocation -> {
            try {
                throw new RuntimeException("failed");
            } finally {
                latch.countDown();
            }
        }).when(originalFoo)
          .bar(any(A.class), any(B.class), any(ConcurrentSkipListMap.class), anyInt(), anyInt());

        MyService myService = new MyService(foo);
        myService.execute();
        
        // 等待异步任务执行完成
        latch.await(5, TimeUnit.SECONDS);

        // 验证bar方法是否被调用
        Mockito.verify(originalFoo, Mockito.times(1))
               .bar(any(A.class), any(B.class), any(ConcurrentSkipListMap.class), anyInt(), anyInt());
    }
}

注意事项

  • 确保静态导入ArgumentMatchers的所有方法:import static org.mockito.ArgumentMatchers.*;,避免参数匹配器的使用错误。
  • 由于@Async方法是异步执行的,测试中需要使用CountDownLatch或其他方式等待异步任务完成,否则测试可能在任务执行前就结束,导致断言失效。

内容的提问来源于stack exchange,提问作者zeeshan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 07:33:12