当前位置: 首页 > 知识库问答 >
问题:

使用Spring WebClient with retry时使用StepVerifier块进行测试

左丘耀
2023-03-14

编辑:这里https://github.com/wujek-srujek/reactor-retry-test是一个包含所有代码的存储库。

我有以下SpringWebClient代码要发布到远程服务器(为了简洁起见,没有导入Kotlin代码):

private val logger = KotlinLogging.logger {}

@Component
class Client(private val webClient: WebClient) {

    companion object {
        const val maxRetries = 2L
        val firstBackOff = Duration.ofSeconds(5L)
        val maxBackOff = Duration.ofSeconds(20L)
    }

    fun send(uri: URI, data: Data): Mono<Void> {
        return webClient
            .post()
            .uri(uri)
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(data)
            .retrieve()
            .toBodilessEntity()
            .doOnSubscribe {
                logger.info { "Calling backend, uri: $uri" }
            }
            .retryExponentialBackoff(maxRetries, firstBackOff, maxBackOff, jitter = false) {
                logger.debug { "Call to $uri failed, will retry (#${it.iteration()} of max $maxRetries)" }
            }
            .doOnError {
                logger.error { "Call to $uri with $maxRetries retries failed with $it" }
            }
            .doOnSuccess {
                logger.info { "Call to $uri succeeded" }
            }
            .then()
    }
}

(它返回一个空的mono,因为我们不期望得到答案,也不关心它。)

我想测试两个案例,其中一个让我头疼,即我想测试的所有重试都被解雇了。我们使用的是mockwebserver(https://github.com/square/okhttp/tree/master/mockwebserver)和reactor-test中的stepverifier。(成功的测试很简单,不需要任何虚拟时间调度器的魔力,而且工作得很好。)以下是失败的代码:

@JsonTest
@ContextConfiguration(classes = [Client::class, ClientConfiguration::class])
class ClientITest @Autowired constructor(
    private val client: Client
) {
    lateinit var server: MockWebServer

    @BeforeEach
    fun `init mock server`() {
        server = MockWebServer()
        server.start()
    }

    @AfterEach
    fun `shutdown server`() {
        server.shutdown()
    }

   @Test
   fun `server call is retried and eventually fails`() {
       val data = Data()
       val uri = server.url("/server").uri()
       val responseStatus = HttpStatus.INTERNAL_SERVER_ERROR

       repeat((0..Client.maxRetries).count()) {
           server.enqueue(MockResponse().setResponseCode(responseStatus.value()))
       }

       StepVerifier.withVirtualTime { client.send(uri, data) }
           .expectSubscription()
           .thenAwait(Duration.ofSeconds(10)) // wait for the first retry
           .expectNextCount(0)
           .thenAwait(Duration.ofSeconds(20)) // wait for the second retry
           .expectNextCount(0)
           .expectErrorMatches {
               val cause = it.cause
               it is RetryExhaustedException &&
                       cause is WebClientResponseException &&
                       cause.statusCode == responseStatus
           }
           .verify()

       // assertions
       }
   }

我使用WithVirtualTime,因为我不希望测试花费近几秒的时间。问题是测试无限期地阻塞。以下是(简化的)日志输出:

okhttp3.mockwebserver.MockWebServer      : MockWebServer[51058] starting to accept connections
Calling backend, uri: http://localhost:51058/server
MockWebServer[51058] received request: POST /server HTTP/1.1 and responded: HTTP/1.1 500 Server Error
Call to http://localhost:51058/server failed, will retry (#1 of max 2)
Calling backend, uri: http://localhost:51058/server
MockWebServer[51058] received request: POST /server HTTP/1.1 and responded: HTTP/1.1 500 Server Error
Call to http://localhost:51058/server failed, will retry (#2 of max 2)

以下使用stepverifier但不使用WebClient的测试工作正常,即使重试次数较多:

@Test
fun test() {
    StepVerifier.withVirtualTime {
        Mono
            .error<RuntimeException>(RuntimeException())
            .retryExponentialBackoff(5,
                                     Duration.ofSeconds(5),
                                     Duration.ofMinutes(2),
                                     jitter = true) {
                println("Retrying")
            }
            .then()
    }
        .expectSubscription()
        .thenAwait(Duration.ofDays(1)) // doesn't matter
        .expectNextCount(0)
        .expectError()
        .verify()
}

谁能帮我修复测试,最好解释出了什么问题?

共有1个答案

夹谷浩宕
2023-03-14

这是对虚拟时间和stepverifier中时钟操作方式的限制。TheNaWait方法与基础调度不同步(例如,RetryBackoff操作的一部分)。这意味着操作员在时钟已经提前一天的点提交重试任务。因此,第二次重试被安排在+1天10秒,因为时钟是+1天。在此之后,时钟从不提前,因此不会向MockWebServer发出额外的请求。

您的情况变得更加复杂,因为涉及到一个额外的组件,MockWebServer,它仍然“实时”工作。尽管推进虚拟时钟是一个非常快速的操作,但来自MockWebServer的响应仍然通过套接字,因此重试调度有一定的延迟,从测试编写的角度来看,这使得事情变得更加复杂。

一个可能的解决方案是将VirtualTimeScheduler的创建外部化,并在并行线程中将AdvanceTimeBy调用绑定到MockServer.takerequest()

 类似资料:
  • 问题内容: 我正在尝试使用Swift 2的新声明将我的类暴露给测试目标。但是我收到此编译器错误: 是包含我要公开的类的模块。如何摆脱这个错误? 问题答案: 在主要目标中,您需要将构建选项设置为“是”。 根据下面@earnshavian的评论,应仅根据苹果发行说明在调试版本中使用此选项:“启用可测试性版本设置应仅在Debug配置中使用,因为它禁止不依赖于不从内部导出内部符号的优化应用或框架” htt

  • 我将使用托管进行实时测试,但我想保护访问并防止搜索引擎索引。例如(服务器目录结构)public_html: _private _bin _cnf _log _...(更多默认目录托管) testPublic css 图像 index.html < 我想index.html是可见的每个人和所有其他目录(除了testPublic)是隐藏的,受保护的访问和搜索引擎不索引。 目录"testPublic"我

  • 简介 在之前的章节我们实现了一个简单但是功能齐全的web项目、学习了如何使用Gradle来构建和运行这个项目。测试代码是软件开发周期中非常重要的一环,能够确保软件的行为能符合预期。这一章我将讲述如何使用Gradle来组织、配置和执行测试代码,学习如何写单元测试、集成测试和功能测试并把他们集成到项目构建中。 Gradle集成了很多Java和Groovy测试框架,在本章的最后你会用JUnit、Test

  • 本节课将介绍如何使用specs —— 一个Scala行为驱动设计(BDD)框架,来进行测试。 扩展规格 让我们直接开始。 import org.specs._ object ArithmeticSpec extends Specification { "Arithmetic" should { "add two numbers" in { 1 + 1 mustEqual

  • 在我正在开发的应用程序中,我需要知道一串单词是否是名词短语、动词短语等。我知道NP和VP既不是依赖项,也不是位置。我也知道要做到这一点,我可能需要某种分块工具,但我找不到任何开源工具。 在SyntaxNet输出的“她真的很喜欢可爱的黑狗”一句中: 我注意到NP“可爱的黑狗”已经放在了自己的树节点中: 所以我想知道是否有任何方法可以将SyntaxNet用作chunker?

  • 我试图测试一个基于Spring引导的Restendpoint。代码能够返回预期的输出,但测试失败,错误如下: 已解析[org.springframework.http.converter.HttpMessageNotWritableException:预设内容类型为“null”的[class java.util.LinkedList]没有转换器 对此有任何想法都将不胜感激! 下面是相同的代码: 控