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

我们继续上一节,继续使用 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(下)的更多相关文章
- SpringCloud升级之路2020.0.x版-40. spock 单元测试封装的 WebClient(上)
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 我们来测试下前面封装好的 WebClient,这里开始,我们使用 spock 编写 gro ...
- SpringCloud升级之路2020.0.x版-1.背景
本系列为之前系列的整理重启版,随着项目的发展以及项目中的使用,之前系列里面很多东西发生了变化,并且还有一些东西之前系列并没有提到,所以重启这个系列重新整理下,欢迎各位留言交流,谢谢!~ Spring ...
- SpringCloud升级之路2020.0.x版-41. SpringCloudGateway 基本流程讲解(1)
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 接下来,将进入我们升级之路的又一大模块,即网关模块.网关模块我们废弃了已经进入维护状态的 ...
- SpringCloud升级之路2020.0.x版-6.微服务特性相关的依赖说明
本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford spring-cl ...
- SpringCloud升级之路2020.0.x版-10.使用Log4j2以及一些核心配置
本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 我们使用 Log4 ...
- SpringCloud升级之路2020.0.x版-43.为何 SpringCloudGateway 中会有链路信息丢失
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 在开始编写我们自己的日志 Filter 之前,还有一个问题我想在这里和大家分享,即在 Sp ...
- SpringCloud升级之路2020.0.x版-29.Spring Cloud OpenFeign 的解析(1)
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 在使用云原生的很多微服务中,比较小规模的可能直接依靠云服务中的负载均衡器进行内部域名与服务 ...
- SpringCloud升级之路2020.0.x版-34.验证重试配置正确性(1)
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 在前面一节,我们利用 resilience4j 粘合了 OpenFeign 实现了断路器. ...
- SpringCloud升级之路2020.0.x版-2.微服务框架需要考虑的问题
本系列为之前系列的整理重启版,随着项目的发展以及项目中的使用,之前系列里面很多东西发生了变化,并且还有一些东西之前系列并没有提到,所以重启这个系列重新整理下,欢迎各位留言交流,谢谢!~ 上图中演示了一 ...
随机推荐
- Java实现红黑树(平衡二叉树)
前言 在实现红黑树之前,我们先来了解一下符号表. 符号表的描述借鉴了Algorithms第四版,详情在:https://algs4.cs.princeton.edu/home/ 符号表有时候被称为字典 ...
- 第29篇-调用Java主类的main()方法
在第1篇中大概介绍过Java中主类方法main()的调用过程,这一篇介绍的详细一点,大概的调用过程如下图所示. 其中浅红色的函数由主线程执行,而另外的浅绿色部分由另外一个线程执行,这个线程最终也会负责 ...
- 9.亿级流量电商系统JVM模型参数预估方案
1. 需求分析 大促在即,拥有亿级流量的电商平台开发了一个订单系统,我们应该如何来预估其并发量?如何根据并发量来合理配置JVM参数呢? 假设,现在有一个场景,一个电商平台,比如京东,需要承担每天上亿的 ...
- cf1082D Maximum Diameter Graph(构造+模拟+细节)
QWQ不得不说 \(cf\)的\(edu\ round\)出这种东西 有点太恶心了 题目大意:给你\(n\)个点,告诉你每个点的最大度数值(也就是说你的度数要小于等于这个),让你构造一个无向图,使其满 ...
- pycharm设置文件中显示模板内容
pycharm中设置自己的文件模板 File>>Settings>>Editor>>File and Code Templates 选择文件类型或者输入文件类型 ...
- Ubuntu安装 配置GCC和Vim
VMware14.0和Ubuntu镜像安装 https://mp.weixin.qq.com/s/045wrbdv92PkUQn2abddvA 适应屏幕设置 按照下列流程检查虚拟机设置 查看 -> ...
- 一文彻底搞通TCP之send & recv原理
接触过网络开发的人,大抵都知道,上层应用使用send函数发送数据,使用recv来接收数据,而send和recv的实现原理又是怎样的呢? 在前面的几篇文章中,我们有提过,TCP是个可靠的.全双工协议.其 ...
- 【转】C语言 printf格式控制符 完全解析
printf的格式控制的完整格式:% - 0 m.n l或h 格式字符下面对组成格式说明的各项加以说明:①%:表示格式说明的起始符号,不可缺少.②-:有-表示左 ...
- 3.2 Dependencies of the Projects in the Solution 解决方案中项目间的依赖项
3.2 Dependencies of the Projects in the Solution 解决方案中项目间的依赖项 The diagram below shows the essential ...
- CSP-S 2021 遗言
感谢€€£,谢谢宁嘞! 第一题,€€£给了很多限制条件,什么"先到先得"."只有一个跑道",让它看起来很好做,然后来骗,来偷袭,广大"消费者" ...