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.微服务框架需要考虑的问题
本系列为之前系列的整理重启版,随着项目的发展以及项目中的使用,之前系列里面很多东西发生了变化,并且还有一些东西之前系列并没有提到,所以重启这个系列重新整理下,欢迎各位留言交流,谢谢!~ 上图中演示了一 ...
随机推荐
- 用C++实现的数独解题程序 SudokuSolver 2.2 及实例分析
SudokuSolver 2.2 程序实现 根据 用C++实现的数独解题程序 SudokuSolver 2.1 及实例分析 里分析,对 2.1 版做了一些改进和尝试. CQuizDealer 类声明部 ...
- Java(12)方法的重载
作者:季沐测试笔记 原文地址:https://www.cnblogs.com/testero/p/15201592.html 博客主页:https://www.cnblogs.com/testero ...
- 浅尝装饰器--property装饰器
[写在前面] 本帖归属于装饰器单元的学习,可以点击关键词'装饰器'查看其他博文讲解 [正文部分] property属性:将类方法用类属性的形式进行调用 class Good: def __init__ ...
- javascript-jquery的基本方法
1.去除字符串中两端的空格$.trim(str) var str1=" 123 " $.trim(str1);//123 2.遍历对象的数据并进行操作$.each(obj,func ...
- Selenium获取动态图片验证码
Selenium获取动态图片验证码 关于图片验证码的文章,我想大家都有一定的了解了. 在我们做UI自动化的时候,经常会遇到图片验证码的问题. 当开发不给咱们提供万能验证码,或者测试第三方网站比如知乎的 ...
- Codeforces Round #748 (Div. 3)
Codeforces Round #748 (Div. 3) A. Elections 思路分析: 令当前值比最大值大即可,如果最大值是它自己,就输出\(0\) 代码 #include <bit ...
- Beta Scrum Meeting汇总
第0次Alpha Scrum Meeting 第1次Alpha Scrum Meeting 第2次Alpha Scrum Meeting 第3次Alpha Scrum Meeting 第4次Alpha ...
- 6月6日 Scrum Meeting
日期:2021年6月6日 会议主要内容概述: 删除模板选择页面,画图页面新增模板选择 保存时后端判重 后端要新增数据分享url 主题色->lxd:画图教程->lsp:表格数据分析-> ...
- hdu 1503 Advanced Fruits(DP)
题意: 将两个英文单词进行合并.[最长公共子串只要保留一份] 输出合并后的英文单词. 思路: 求最长公共子串. 记录路径: mark[i][j]=-1:从mark[i-1][j]转移而来. mark[ ...
- RocketMQ源码详解 | Broker篇 · 其二:文件系统
概述 在 Broker 的通用请求处理器将一个消息进行分发后,就来到了 Broker 的专门处理消息存储的业务处理器部分.本篇文章,我们将要探讨关于 RocketMQ 高效的原因之一:文件结构的良好设 ...