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

JUnit5测试SpringBoot控制器时无法触发预期自定义异常

问题描述

作为JUnit新手,在为SpringBoot控制器编写测试用例时遇到以下问题:

  • 控制器调用TokenValidator类的validateUser(void方法),预期该方法抛出自定义AddressBookException,从而返回400 Bad Request响应
  • 实际测试中跳过异常逻辑,返回200 OK响应
  • 已将when().thenThrow()改为void方法对应的doThrow(),但问题仍未解决

相关代码

测试类

@SpringBootTest
@RunWith(MockitoJUnitRunner.class)
@AutoConfigureMockMvc
class GetUserAddressControllerTest {
    @Mock
    private CustomerAddressService customerAddressService;

    @Mock
    private TokenValidator tokenValidator;

    @InjectMocks
    private GetUserAddressController getUserAddressController;

    @Autowired
    private MockMvc mockMvc;

    @BeforeEach
    void setup() {
        MockitoAnnotations.initMocks(this);
        mockMvc = MockMvcBuilders.standaloneSetup(getUserAddressController).build();
    }

    @Test
    void getAllAddressesFailureTest() throws Exception {
        doThrow(new AddressBookException(RestResultCode.BAD_REQUEST, ErrorType.UNAUTHORIZED_ERROR.getStatus(), "Access is denied"))
                .when(tokenValidator)
                .validateUser(header, userId);


        MvcResult mvcResult = mockMvc.perform(get(getAddressUrl)
                        .param("userId", userId)
                        .header("Authorization", header))
                .andDo(MockMvcResultHandlers.print())
                .andExpect(status().is4xxClientError())
                .andReturn();

        verify(customerAddressService).getAllAddresses(any());
    }
}

控制器类

@ApiOperation(value = "This API will fetch all address for a user")
@GetMapping(value = "/addresses", produces = MediaType.APPLICATION_JSON_VALUE)
CustomerAddressResponse getAllAddresses(@RequestHeader(value = "Authorization", required = true) String authToken,
                                        @RequestParam("userId") String userId) {

    tokenValidator.validateUser(authToken, userId);  //----> 期望抛出异常
    return customerAddressService.getAllAddresses(userId);
}

TokenValidator类

public void validateUser(String authToken, String uuid) {
    if (StringUtils.isEmpty(authToken)) {
        throw new AddressBookException(RestResultCode.UNAUTHORIZED, ErrorType.ACCESS_DENIED_ERROR.getStatus(), "Access is denied");
    }
    // 其他逻辑
}

错误堆栈
.   ____          _            __ _ _
 /\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.1.1.RELEASE)

Hibernate: 
    select
        applicatio0_.id as id1_2_,
        applicatio0_.active as active2_2_,
        applicatio0_.created_ts as created_3_2_,
        applicatio0_.updated_by as updated_4_2_,
        applicatio0_.updated_ts as updated_5_2_,
        applicatio0_.config_key as config_k6_2_,
        applicatio0_.config_value as config_v7_2_ 
    from
        application_configuration applicatio0_ 
    where
        (
            applicatio0_.active= true
        )

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /addressservice/v1/addresses
       Parameters = {userId=[be6cefe2-04ed-4d89-bd25-7342b400272c]}
          Headers = {Authorization=[Bearer 279db47b-a419-40f7-81f3-a8bf755a9dcb]}
             Body = <no character encoding set>
    Session Attrs = {}

Handler:
             Type = com.addressbook.service.controller.GetUserAddressController
           Method = com.addressbook.service.response.CustomerAddressResponse 
com.addressbook.service.controller.GetUserAddressController.getAllAddresses(java.lang.String,java.lang.String)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {}
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

Range for response status value 200 expected:<CLIENT_ERROR> but was:<SUCCESSFUL>
Expected :CLIENT_ERROR
Actual   :SUCCESSFUL
<Click to see difference>

java.lang.AssertionError: Range for response status value 200 expected:<CLIENT_ERROR> but was:<SUCCESSFUL>
    at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:55)
    at org.springframework.test.util.AssertionErrors.assertEquals(AssertionErrors.java:82)
    at org.springframework.test.web.servlet.result.StatusResultMatchers.lambda$is4xxClientError$5(StatusResultMatchers.java:94)
    at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.java:195)
    at com.addressbook.service.controller.GetUserAddressControllerTest.getAllAddressesFailureTest(GetUserAddressControllerTest.java:150)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:532)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:115)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:170)
    at org.junit.jupiter.engine.execution.ThrowableCollector.execute(ThrowableCollector.java:40)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:166)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:113)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:58)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$4(NodeTestTask.java:108)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:72)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:98)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:74)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$4(NodeTestTask.java:112)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:72)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:98)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:74)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$4(NodeTestTask.java:112)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:72)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:98)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:74)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:220)
    at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute$6(DefaultLauncher.java:188)
    at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:202)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:181)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:128)
    at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:102)
    at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:82)
    at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:78)
    at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:61)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
    at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
    at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
    at com.sun.proxy.$Proxy5.stop(Unknown Source)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.stop(TestWorker.java:132)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
    at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:182)
    at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:164)
    at org.gradle.internal.remote.internal.hub.MessageHub$Handler.run(MessageHub.java:412)
    at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
    at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:48)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:56)
    at java.base/java.lang.Thread.run(Thread.java:829)


com.addressbook.service.controller.GetUserAddressControllerTest > getAllAddressesFailureTest() FAILED
    java.lang.AssertionError at GetUserAddressControllerTest.java:150
Disconnected from the target VM, address: 'localhost:53204', transport: 'socket'

解决方案

1. 修正测试类的注解与MockMvc初始化冲突

  • 移除@SpringBootTest和@AutoConfigureMockMvc,改用纯Mockito+MockMvc的独立测试模式,避免Spring容器自动注入的控制器与@InjectMocks创建的实例不一致
  • 移除@Autowired private MockMvc mockMvc;,仅保留手动创建的实例
  • 移除@RunWith(MockitoJUnitRunner.class),JUnit 5无需此注解,改用@ExtendWith(MockitoExtension.class)

修改后的测试类头部:

@ExtendWith(MockitoExtension.class)
class GetUserAddressControllerTest {
    // ... 其他代码不变
}

2. 修正Mock方法的参数匹配逻辑

如果测试中header和userId变量与请求传入的实际值不一致,Mock的触发条件不会满足,改用any()匹配任意参数:

doThrow(new AddressBookException(RestResultCode.BAD_REQUEST, ErrorType.UNAUTHORIZED_ERROR.getStatus(), "Access is denied"))
        .when(tokenValidator)
        .validateUser(anyString(), anyString());

3. 确保自定义异常被Spring MVC捕获并转换为HTTP错误响应

创建全局异常处理器,将AddressBookException转换为对应的400状态码:

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(AddressBookException.class)
    public ResponseEntity<ErrorResponse> handleAddressBookException(AddressBookException ex) {
        ErrorResponse errorResponse = new ErrorResponse(ex.getCode(), ex.getMessage());
        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
    }
}

4. 修正verify断言逻辑

预期抛出异常后,customerAddressService.getAllAddresses()不应该被调用,所以修改verify为:

verify(customerAddressService, never()).getAllAddresses(anyString());

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 17:31:58