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

Quarkus响应式流onFailure()分支异常场景单元测试方案

Quarkus Reactive Panache响应式接口异常场景单元测试失败问题

问题描述

使用集成Mongo Reactive Panache的Quarkus构建响应式REST接口时,实现人员创建逻辑:若提交的人员邮箱已存在则抛出BusinessException,通过Mutiny的onFailure()回调捕获异常,返回封装了AStatus错误信息的服务端错误响应。
目前接口成功路径的单元测试已验证通过,但异常场景测试始终无法正确触发:

  • 使用@InjectMock注解Mock PersonRepository的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的返回值中。

修复方案

  1. 修正异常定义:确保BusinessException继承RuntimeException,属于非受检异常,符合响应式编程的异常传递惯例。
  2. 修正Mock逻辑:不要让persist方法同步抛出异常,而是返回一个携带失败信号的Uni对象,让异常进入响应式管道被onFailure处理。将测试中异常场景的Mock代码替换为:
    when(repository.persist(any(Person.class)))
            .thenReturn(Uni.createFrom().failure(new BusinessException("邮箱已存在")));
    
  3. 修正测试断言逻辑:异常场景下接口返回的实体是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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.03 01:45:43