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

如何验证WebFlux异步调用是否触发指定异常?

测试异步WebClient错误处理器的方案

针对你这种异步无返回值方法的错误处理器测试需求,有几种不用改成block()的可行方案,根据是否允许修改原服务代码来选择:


方案1:给send方法添加可选错误回调(推荐,侵入性低)

如果可以给原方法加一个可选的错误处理参数,就能在测试中捕获异常,同时保持调用端异步特性:

修改后的服务代码

@Component
class MyService {

    val webClient: WebClient by lazy {
        WebClient.builder()
            .baseUrl("https://localhost:8080")
            .build()
    }

    // 添加可选的错误消费者参数,默认空实现
    fun send(onError: Consumer<Throwable>? = null) {
        webClient.get()
            .uri("/posts")
            .retrieve()
            .onStatus(HttpStatus::isError) { Mono.error { MyException() } }
            .bodyToMono(Void::class.java)
            .subscribe(
                {}, // 成功回调空实现
                { error -> onError?.accept(error) } // 触发传入的错误回调
            )
    }
}

测试代码

用CountDownLatch等待异步操作完成,同时捕获异常:

@Test
fun testErrorHandlerTriggersMyException() {
    mockWebServer.enqueue(MockResponse()
        .setResponseCode(HttpStatus.INTERNAL_SERVER_ERROR.value())
        .addHeader("Content-Type", "application/json")
    )

    val latch = CountDownLatch(1)
    var capturedException: Throwable? = null

    // 调用send时传入自定义错误回调
    myService.send { error ->
        capturedException = error
        latch.countDown() // 通知测试线程可以继续
    }

    // 等待异步操作完成,设置5秒超时避免死等
    assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue()

    // 验证捕获的异常类型
    assertThat(capturedException).isInstanceOf(MyException::class.java)
}

方案2:暴露内部Mono供测试(需修改方法签名)

如果允许让send方法返回内部的Mono,可以用Reactor的StepVerifier来优雅测试:

修改后的服务代码

@Component
class MyService {

    val webClient: WebClient by lazy {
        WebClient.builder()
            .baseUrl("https://localhost:8080")
            .build()
    }

    // 返回内部的Mono,调用端仍可选择异步subscribe
    fun send(): Mono<Void> {
        val requestMono = webClient.get()
            .uri("/posts")
            .retrieve()
            .onStatus(HttpStatus::isError) { Mono.error { MyException() } }
            .bodyToMono(Void::class.java)
        requestMono.subscribe() // 保持原异步逻辑
        return requestMono
    }
}

测试代码

@Test
fun testErrorHandlerTriggersMyException() {
    mockWebServer.enqueue(MockResponse()
        .setResponseCode(HttpStatus.INTERNAL_SERVER_ERROR.value())
        .addHeader("Content-Type", "application/json")
    )

    val requestMono = myService.send()

    // 用StepVerifier验证Mono是否抛出指定异常
    StepVerifier.create(requestMono)
        .expectError(MyException::class.java)
        .verify()
}

方案3:全局Hook捕获异常(无需修改服务代码)

如果完全不能修改原服务代码,可以通过Reactor的全局错误Hook来捕获异常,但要注意测试后清理Hook避免影响其他用例:

测试代码

@Test
fun testErrorHandlerTriggersMyException() {
    mockWebServer.enqueue(MockResponse()
        .setResponseCode(HttpStatus.INTERNAL_SERVER_ERROR.value())
        .addHeader("Content-Type", "application/json")
    )

    val latch = CountDownLatch(1)
    var capturedException: Throwable? = null

    // 注册全局错误Hook,捕获异常
    val originalHook = Hooks.onOperatorError { _, error ->
        capturedException = error
        latch.countDown()
        error // 抛出原异常,不影响原有逻辑
    }

    try {
        myService.send()
        // 等待异步操作完成
        assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue()
        // 验证异常类型
        assertThat(capturedException).isInstanceOf(MyException::class.java)
    } finally {
        // 必须恢复原Hook,避免污染其他测试
        Hooks.resetOnOperatorError()
    }
}

内容的提问来源于stack exchange,提问作者J.Kennsy

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 09:13:20