本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent

我们继续上一节,继续使用 spock 测试我们自己封装的 WebClient

测试针对 readTimeout 重试

针对响应超时,我们需要验证重试仅针对可以重试的方法(包括 GET 方法以及配置的可重试方法),针对不可重试的方法没有重试。我们可以通过 spock 单元测试中,检查对于负载均衡器获取实例方法的调用次数看出来是否有重试

我们通过 httpbin.org 的 '/delay/秒' 实现 readTimeout,分别验证:

  • 测试 GET 延迟 2 秒返回,超过读取超时,这时候会重试
  • 测试 POST 延迟 3 秒返回,超过读取超时,同时路径在重试路径中,这样也是会重试的
  • 测试 POST 延迟 2 秒返回,超过读取超时,同时路径在重试路径中,这样不会重试

代码如下:

@SpringBootTest(
properties = [
"webclient.configs.testServiceWithCannotConnect.baseUrl=http://testServiceWithCannotConnect",
"webclient.configs.testServiceWithCannotConnect.serviceName=testServiceWithCannotConnect",
"webclient.configs.testService.baseUrl=http://testService",
"webclient.configs.testService.serviceName=testService",
"webclient.configs.testService.responseTimeout=1s",
"webclient.configs.testService.retryablePaths[0]=/delay/3",
"webclient.configs.testService.retryablePaths[1]=/status/4*",
"spring.cloud.loadbalancer.zone=zone1",
"resilience4j.retry.configs.default.maxAttempts=3",
"resilience4j.circuitbreaker.configs.default.failureRateThreshold=50",
"resilience4j.circuitbreaker.configs.default.slidingWindowType=TIME_BASED",
"resilience4j.circuitbreaker.configs.default.slidingWindowSize=5",
//因为重试是 3 次,为了防止断路器打开影响测试,设置为正好比重试多一次的次数,防止触发
//同时我们在测试的时候也需要手动清空断路器统计
"resilience4j.circuitbreaker.configs.default.minimumNumberOfCalls=4",
"resilience4j.circuitbreaker.configs.default.recordExceptions=java.lang.Exception"
],
classes = MockConfig
)
class WebClientUnitTest extends Specification {
@SpringBootApplication
static class MockConfig {
}
@SpringBean
private LoadBalancerClientFactory loadBalancerClientFactory = Mock() @Autowired
private CircuitBreakerRegistry circuitBreakerRegistry
@Autowired
private Tracer tracer
@Autowired
private ServiceInstanceMetrics serviceInstanceMetrics
@Autowired
private WebClientNamedContextFactory webClientNamedContextFactory //不同的测试方法的类对象不是同一个对象,会重新生成,保证互相没有影响
def zone1Instance1 = new DefaultServiceInstance(instanceId: "instance1", host: "www.httpbin.org", port: 80, metadata: Map.ofEntries(Map.entry("zone", "zone1")))
def zone1Instance2 = new DefaultServiceInstance(instanceId: "instance2", host: "www.httpbin.org", port: 8081, metadata: Map.ofEntries(Map.entry("zone", "zone1")))
def zone1Instance3 = new DefaultServiceInstance(instanceId: "instance3", host: "httpbin.org", port: 80, metadata: Map.ofEntries(Map.entry("zone", "zone1")))
RoundRobinWithRequestSeparatedPositionLoadBalancer loadBalancerClientFactoryInstance = Spy();
ServiceInstanceListSupplier serviceInstanceListSupplier = Spy(); //所有测试的方法执行前会调用的方法
def setup() {
//初始化 loadBalancerClientFactoryInstance 负载均衡器
loadBalancerClientFactoryInstance.setTracer(tracer)
loadBalancerClientFactoryInstance.setServiceInstanceMetrics(serviceInstanceMetrics)
loadBalancerClientFactoryInstance.setServiceInstanceListSupplier(serviceInstanceListSupplier)
} def "测试针对 readTimeout 重试"() {
given: "设置 testService 的实例都是正常实例"
loadBalancerClientFactory.getInstance("testService") >> loadBalancerClientFactoryInstance
serviceInstanceListSupplier.get() >> Flux.just(Lists.newArrayList(zone1Instance1, zone1Instance3))
when: "测试 GET 延迟 2 秒返回,超过读取超时"
//清除断路器影响
circuitBreakerRegistry.getAllCircuitBreakers().forEach({ c -> c.reset() })
try {
webClientNamedContextFactory.getWebClient("testService")
.get().uri("/delay/2").retrieve()
.bodyToMono(String.class).block();
} catch (WebClientRequestException e) {
if (e.getCause() in ReadTimeoutException) {
//读取超时忽略
} else {
throw e;
}
}
then: "每次都会超时所以会重试,根据配置一共有 3 次"
3 * loadBalancerClientFactoryInstance.getInstanceResponseByRoundRobin(*_)
when: "测试 POST 延迟 3 秒返回,超过读取超时,同时路径在重试路径中"
//清除断路器影响
circuitBreakerRegistry.getAllCircuitBreakers().forEach({ c -> c.reset() })
try {
webClientNamedContextFactory.getWebClient("testService")
.post().uri("/delay/3").retrieve()
.bodyToMono(String.class).block();
} catch (WebClientRequestException e) {
if (e.getCause() in ReadTimeoutException) {
//读取超时忽略
} else {
throw e;
}
}
then: "每次都会超时所以会重试,根据配置一共有 3 次"
3 * loadBalancerClientFactoryInstance.getInstanceResponseByRoundRobin(*_)
when: "测试 POST 延迟 2 秒返回,超过读取超时,这个不能重试"
//清除断路器影响
circuitBreakerRegistry.getAllCircuitBreakers().forEach({ c -> c.reset() })
try {
webClientNamedContextFactory.getWebClient("testService")
.post().uri("/delay/2").retrieve()
.bodyToMono(String.class).block();
} catch (WebClientRequestException e) {
if (e.getCause() in ReadTimeoutException) {
//读取超时忽略
} else {
throw e;
}
}
then: "没有重试,只有一次调用"
1 * loadBalancerClientFactoryInstance.getInstanceResponseByRoundRobin(*_)
}
}

测试非 2xx 响应码返回的重试

对于非 2xx 的响应码,代表请求失败,我们需要测试:

  • 测试 GET 返回 500,会有重试
  • 测试 POST 返回 500,没有重试
  • 测试 POST 返回 400,这个请求路径在重试路径中,会有重试
@SpringBootTest(
properties = [
"webclient.configs.testServiceWithCannotConnect.baseUrl=http://testServiceWithCannotConnect",
"webclient.configs.testServiceWithCannotConnect.serviceName=testServiceWithCannotConnect",
"webclient.configs.testService.baseUrl=http://testService",
"webclient.configs.testService.serviceName=testService",
"webclient.configs.testService.responseTimeout=1s",
"webclient.configs.testService.retryablePaths[0]=/delay/3",
"webclient.configs.testService.retryablePaths[1]=/status/4*",
"spring.cloud.loadbalancer.zone=zone1",
"resilience4j.retry.configs.default.maxAttempts=3",
"resilience4j.circuitbreaker.configs.default.failureRateThreshold=50",
"resilience4j.circuitbreaker.configs.default.slidingWindowType=TIME_BASED",
"resilience4j.circuitbreaker.configs.default.slidingWindowSize=5",
//因为重试是 3 次,为了防止断路器打开影响测试,设置为正好比重试多一次的次数,防止触发
//同时我们在测试的时候也需要手动清空断路器统计
"resilience4j.circuitbreaker.configs.default.minimumNumberOfCalls=4",
"resilience4j.circuitbreaker.configs.default.recordExceptions=java.lang.Exception"
],
classes = MockConfig
)
class WebClientUnitTest extends Specification {
@SpringBootApplication
static class MockConfig {
}
@SpringBean
private LoadBalancerClientFactory loadBalancerClientFactory = Mock() @Autowired
private CircuitBreakerRegistry circuitBreakerRegistry
@Autowired
private Tracer tracer
@Autowired
private ServiceInstanceMetrics serviceInstanceMetrics
@Autowired
private WebClientNamedContextFactory webClientNamedContextFactory //不同的测试方法的类对象不是同一个对象,会重新生成,保证互相没有影响
def zone1Instance1 = new DefaultServiceInstance(instanceId: "instance1", host: "www.httpbin.org", port: 80, metadata: Map.ofEntries(Map.entry("zone", "zone1")))
def zone1Instance2 = new DefaultServiceInstance(instanceId: "instance2", host: "www.httpbin.org", port: 8081, metadata: Map.ofEntries(Map.entry("zone", "zone1")))
def zone1Instance3 = new DefaultServiceInstance(instanceId: "instance3", host: "httpbin.org", port: 80, metadata: Map.ofEntries(Map.entry("zone", "zone1")))
RoundRobinWithRequestSeparatedPositionLoadBalancer loadBalancerClientFactoryInstance = Spy();
ServiceInstanceListSupplier serviceInstanceListSupplier = Spy(); //所有测试的方法执行前会调用的方法
def setup() {
//初始化 loadBalancerClientFactoryInstance 负载均衡器
loadBalancerClientFactoryInstance.setTracer(tracer)
loadBalancerClientFactoryInstance.setServiceInstanceMetrics(serviceInstanceMetrics)
loadBalancerClientFactoryInstance.setServiceInstanceListSupplier(serviceInstanceListSupplier)
} def "测试非 200 响应码返回" () {
given: "设置 testService 的实例都是正常实例"
loadBalancerClientFactory.getInstance("testService") >> loadBalancerClientFactoryInstance
serviceInstanceListSupplier.get() >> Flux.just(Lists.newArrayList(zone1Instance1, zone1Instance3))
when: "测试 GET 返回 500"
//清除断路器影响
circuitBreakerRegistry.getAllCircuitBreakers().forEach({ c -> c.reset() })
try {
webClientNamedContextFactory.getWebClient("testService")
.get().uri("/status/500").retrieve()
.bodyToMono(String.class).block();
} catch (WebClientResponseException e) {
if (e.getStatusCode().is5xxServerError()) {
//5xx忽略
} else {
throw e;
}
}
then: "每次都没有返回 2xx 所以会重试,根据配置一共有 3 次"
3 * loadBalancerClientFactoryInstance.getInstanceResponseByRoundRobin(*_)
when: "测试 POST 返回 500"
//清除断路器影响
circuitBreakerRegistry.getAllCircuitBreakers().forEach({ c -> c.reset() })
try {
webClientNamedContextFactory.getWebClient("testService")
.post().uri("/status/500").retrieve()
.bodyToMono(String.class).block();
} catch (WebClientResponseException e) {
if (e.getStatusCode().is5xxServerError()) {
//5xx忽略
} else {
throw e;
}
}
then: "POST 默认不重试,所以只会调用一次"
1 * loadBalancerClientFactoryInstance.getInstanceResponseByRoundRobin(*_)
when: "测试 POST 返回 400,这个请求路径在重试路径中"
//清除断路器影响
circuitBreakerRegistry.getAllCircuitBreakers().forEach({ c -> c.reset() })
try {
webClientNamedContextFactory.getWebClient("testService")
.post().uri("/status/400").retrieve()
.bodyToMono(String.class).block();
} catch (WebClientResponseException e) {
if (e.getStatusCode().is4xxClientError()) {
//4xx忽略
} else {
throw e;
}
}
then: "路径在重试路径中,所以会重试"
3 * loadBalancerClientFactoryInstance.getInstanceResponseByRoundRobin(*_)
}
}

微信搜索“我的编程喵”关注公众号,每日一刷,轻松提升技术,斩获各种offer

SpringCloud升级之路2020.0.x版-40. spock 单元测试封装的 WebClient(下)的更多相关文章

  1. SpringCloud升级之路2020.0.x版-40. spock 单元测试封装的 WebClient(上)

    本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 我们来测试下前面封装好的 WebClient,这里开始,我们使用 spock 编写 gro ...

  2. SpringCloud升级之路2020.0.x版-1.背景

    本系列为之前系列的整理重启版,随着项目的发展以及项目中的使用,之前系列里面很多东西发生了变化,并且还有一些东西之前系列并没有提到,所以重启这个系列重新整理下,欢迎各位留言交流,谢谢!~ Spring ...

  3. SpringCloud升级之路2020.0.x版-41. SpringCloudGateway 基本流程讲解(1)

    本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 接下来,将进入我们升级之路的又一大模块,即网关模块.网关模块我们废弃了已经进入维护状态的 ...

  4. SpringCloud升级之路2020.0.x版-6.微服务特性相关的依赖说明

    本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford spring-cl ...

  5. SpringCloud升级之路2020.0.x版-10.使用Log4j2以及一些核心配置

    本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 我们使用 Log4 ...

  6. SpringCloud升级之路2020.0.x版-43.为何 SpringCloudGateway 中会有链路信息丢失

    本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 在开始编写我们自己的日志 Filter 之前,还有一个问题我想在这里和大家分享,即在 Sp ...

  7. SpringCloud升级之路2020.0.x版-29.Spring Cloud OpenFeign 的解析(1)

    本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 在使用云原生的很多微服务中,比较小规模的可能直接依靠云服务中的负载均衡器进行内部域名与服务 ...

  8. SpringCloud升级之路2020.0.x版-34.验证重试配置正确性(1)

    本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 在前面一节,我们利用 resilience4j 粘合了 OpenFeign 实现了断路器. ...

  9. SpringCloud升级之路2020.0.x版-2.微服务框架需要考虑的问题

    本系列为之前系列的整理重启版,随着项目的发展以及项目中的使用,之前系列里面很多东西发生了变化,并且还有一些东西之前系列并没有提到,所以重启这个系列重新整理下,欢迎各位留言交流,谢谢!~ 上图中演示了一 ...

随机推荐

  1. 搭建Mac+Java+appium+IOS真机自动化环境

    一.安装前环境准备 1.确保电脑已经有homebrew(包管理器)  下载链接[https://brew.sh/]   2.通过 brew 安装node.js brew install node 安装 ...

  2. 【Java虚拟机4】Java内存模型(硬件层面的并发优化基础知识--缓存一致性问题)

    前言 今天学习了Java内存模型第一课的视频,讲了硬件层面的知识,还是和大学时一样,醍醐灌顶.老师讲得太好了. Java内存模型,感觉以前学得比较抽象.很繁杂,抽象. 这次试着系统一点跟着2个老师学习 ...

  3. 什么是Sprint计划?

    Sprint 计划是Scrum框架中的一个事件,团队将确定他们将在冲刺期间处理的产品积压项目,并讨论他们完成这些产品积压项目的初始计划. 团队可能会发现建立冲刺目标很有帮助,并以此为基础确定他们在冲刺 ...

  4. Egg.js学习与实战系列 · Post请求`csrf token`问题

    在使用axios请求egg.js封装的post接口时出现missing csrf token 或 invalid csrf token.踩过坑的新手估计不在少数,本篇记录一下解决方法. 问题原因 引用 ...

  5. nssm.exe使用方法

    nssm no-sucking service manager 1. 安装服务命令 nssm install <servicename> nssm install <servicen ...

  6. 5.29日 Scrum Metting

    日期:2021年5月29日 会议主要内容概述:人员调整,xyl同时兼顾前后端:确定表格缩放策略和新图表添加:强调任务分配,总结工作. 一.进度情况## 组员 负责 两日内已完成的工作 后两日计划完成的 ...

  7. UltraSoft - Alpha - Scrum Meeting 8

    Date: Apr 23th, 2020. Scrum 情况汇报 进度情况 组员 负责 昨日进度 后两日任务 CookieLau PM.后端 aliyun连接前后端,跑通demo 实现邮箱注册的验证码 ...

  8. [no code][scrum meeting] Alpha 6

    项目 内容 会议时间 2020-04-13 会议主题 后端技术细节分析 会议时长 30min 参会人员 PM+后端组成员 $( "#cnblogs_post_body" ).cat ...

  9. springboot多配置环境

    在我们的开发过程中,经常会有多套配置环境,比如开发环境(dev),测试环境(test),生产环境(prod)等,在各个环境中我们需要使用到不同的配置,那么在springboot中是如何做到的呢? 1. ...

  10. 微信小程序 scroll-view 完成上拉加载更多

    我们经常在软件客户端上看到这么一个功能,当我们阅读信息浏览到文章的末尾时,通常会加载出更多的信息.比如,我们在简书客户端上浏览推荐文章时,浏览到屏幕的末尾,此时又加载出了另一页的推荐文章,即实现了上拉 ...