Quarkus响应式流onFailure()分支异常场景单元测试方案
Quarkus Reactive Panache响应式接口异常场景单元测试失败问题
问题描述
使用集成Mongo Reactive Panache的Quarkus构建响应式REST接口时,实现人员创建逻辑:若提交的人员邮箱已存在则抛出BusinessException,通过Mutiny的onFailure()回调捕获异常,返回封装了AStatus错误信息的服务端错误响应。
目前接口成功路径的单元测试已验证通过,但异常场景测试始终无法正确触发:
- 使用
@InjectMock注解MockPersonRepository的persist方法使其抛出BusinessException时,Mockito抛出异常提示Checked exception is invalid for this method! - 即便替换为抛出
RuntimeException也无法正常执行,始终无法进入onFailure分支完成异常场景的逻辑校验。
相关代码
PersonResource.java
@POST public Uni<Response> create(Person person){ return repository.persist(person).map(r -> Response.ok(r).build()) .onFailure() .recoverWithItem(f-> { AStatus status = createErrorStatus(f.getMessage()); return Response.serverError().entity(status).build(); }); }
PersonResourceTest.java
package com.eventu.resource; import com.eventu.exception.BusinessException; import com.eventu.repository.PersonRepository; import com.eventu.vo.Address; import com.eventu.vo.Person; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.mockito.InjectMock; import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.helpers.test.UniAssertSubscriber; import org.bson.types.ObjectId; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import javax.inject.Inject; import javax.ws.rs.core.Response; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @QuarkusTest class PersonResourceTest { @InjectMock PersonRepository repository; @Inject PersonResource resource; Uni<Person> uniPerson; Person person; @BeforeEach public void setUp(){ person = Person.builder().address(Address.builder() .addressLine_1("1000 Test dr") .addressLine_2("opposite crescent dr").suiteNumber("Ste. 200").city("testCity") .countryCode("US").state("CA") .build()).emailAddress("test@test.com").firstName("John").lastName("Barista") .mobileNumber("70444444444") .id(new ObjectId()) .build(); uniPerson = Uni.createFrom().item(person); when(repository.persist(any(Person.class))).thenReturn(uniPerson); } // 成功路径测试可正常通过 @Test @DisplayName("When a new person object is passed then person object with Id should be returned") void create_success() { final Uni<Response> p = resource.create(person); Person respPerson = (Person) p.subscribe().withSubscriber(UniAssertSubscriber.create()).assertCompleted().getItem().getEntity(); Assertions.assertNotNull(p); Assertions.assertEquals(person, respPerson); } /** * 该测试执行到mock抛出异常的行就失败,错误信息: * org.mockito.exceptions.base.MockitoException: * Checked exception is invalid for this method! * Invalid: com.eventu.exception.BusinessException */ @Test @DisplayName("When a new person object is passed but email exists then AStatus object with error description") void create_duplicate_emailId() { when(repository.persist(any(Person.class))).thenThrow(BusinessException.class); // 此处执行失败,替换为RuntimeException.class也无法正常运行 final Uni<Response> p = resource.create(person); Person respPerson = (Person) p.subscribe().withSubscriber(UniAssertSubscriber.create()).assertCompleted().getItem().getEntity(); // 预期此处返回AStatus类型对象,而非Person Assertions.assertEquals(person, respPerson); } }
问题根因
- 受检异常报错:如果
BusinessException继承自Exception就属于受检异常,而Reactive Panache的persist方法签名本身没有声明抛出任何受检异常,Mockito的校验规则不允许给这类方法Mock抛出未声明的受检异常,因此直接抛出校验错误。 - 替换为RuntimeException仍不触发
onFailure:thenThrow()的作用是让persist方法被调用时直接在调用线程同步抛出异常,这个异常发生在响应式流组装阶段,根本没有被包装进Uni返回值进入Mutiny的处理管道,自然不会被后续的onFailure()回调捕获。响应式编程中所有需要被管道处理的异常,都必须作为失败信号封装在Uni/Multi的返回值中。
修复方案
- 修正异常定义:确保
BusinessException继承RuntimeException,属于非受检异常,符合响应式编程的异常传递惯例。 - 修正Mock逻辑:不要让
persist方法同步抛出异常,而是返回一个携带失败信号的Uni对象,让异常进入响应式管道被onFailure处理。将测试中异常场景的Mock代码替换为:when(repository.persist(any(Person.class))) .thenReturn(Uni.createFrom().failure(new BusinessException("邮箱已存在"))); - 修正测试断言逻辑:异常场景下接口返回的实体是
AStatus类型而非Person,需要对应调整断言逻辑,参考代码如下:@Test @DisplayName("When a new person object is passed but email exists then AStatus object with error description") void create_duplicate_emailId() { when(repository.persist(any(Person.class))) .thenReturn(Uni.createFrom().failure(new BusinessException("邮箱已存在"))); final Uni<Response> resultUni = resource.create(person); Response response = resultUni.subscribe().withSubscriber(UniAssertSubscriber.create()) .assertCompleted() .getItem(); // 校验响应状态为500 Assertions.assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus()); // 校验返回实体为AStatus类型且携带正确错误信息 AStatus errorStatus = (AStatus) response.getEntity(); Assertions.assertEquals("邮箱已存在", errorStatus.getMessage()); }
内容的提问来源于stack exchange,提问作者Dinesh Arora
相关产品推荐
相关产品推荐

