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
相关产品推荐
相关产品推荐

