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

我们来测试下前面封装好的 WebClient,这里开始,我们使用 spock 编写 groovy 单元测试,这种编写出来的单元测试,代码更加简洁,同时更加灵活,我们在接下来的单元测试代码中就能看出来。
编写基于 spock 的 spring-boot context 测试
我们加入前面设计的配置,编写测试类:
@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 {
}
}
我们加入三个服务实例供单元测试调用:
class WebClientUnitTest extends Specification {
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")))
}
我们要动态的指定负载均衡获取服务实例列表的响应,即去 Mock 负载均衡器的 ServiceInstanceListSupplier 并覆盖:
class WebClientUnitTest extends Specification {
@Autowired
private Tracer tracer
@Autowired
private ServiceInstanceMetrics serviceInstanceMetrics
RoundRobinWithRequestSeparatedPositionLoadBalancer loadBalancerClientFactoryInstance = Spy();
ServiceInstanceListSupplier serviceInstanceListSupplier = Spy();
//所有测试的方法执行前会调用的方法
def setup() {
//初始化 loadBalancerClientFactoryInstance 负载均衡器
loadBalancerClientFactoryInstance.setTracer(tracer)
loadBalancerClientFactoryInstance.setServiceInstanceMetrics(serviceInstanceMetrics)
loadBalancerClientFactoryInstance.setServiceInstanceListSupplier(serviceInstanceListSupplier)
}
}
之后,我们可以通过下面的 groovy 代码,动态指定微服务返回实例:
//指定 testService 微服务的 LoadBalancer 为 loadBalancerClientFactoryInstance
loadBalancerClientFactory.getInstance("testService") >> loadBalancerClientFactoryInstance
//指定 testService 微服务实例列表为 zone1Instance1, zone1Instance3
serviceInstanceListSupplier.get() >> Flux.just(Lists.newArrayList(zone1Instance1, zone1Instance3))
测试断路器异常重试以及断路器级别
我们需要验证:
- 对于断路器打开的异常,由于没有请求发出去,所以需要直接重试其他的实例。我们可以设立一个微服务,包含两个实例,将其中一个实例的某个路径断路器打开,之后多次调用这个微服务的这个路径接口,看是否都调用成功(由于有重试,所以每次调用都会成功)。同时验证,对于负载均衡器获取服务实例的调用,多于调用次数(每次重试都会调用负载均衡器获取一个新的实例用于调用)
- 某个路径断路器打开的时候,其他路径断路器不会打开。在上面打开一个微服务某个实例的一个路径的断路器之后,我们调用其他的路径,无论多少次,都成功并且调用负载均衡器获取服务实例的次数等于调用次数,代表没有重试,也就是没有断路器异常。
编写代码:
@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 "测试断路器异常重试以及断路器级别"() {
given: "设置 testService 的实例都是正常实例"
loadBalancerClientFactory.getInstance("testService") >> loadBalancerClientFactoryInstance
serviceInstanceListSupplier.get() >> Flux.just(Lists.newArrayList(zone1Instance1, zone1Instance3))
when: "断路器打开"
//清除断路器影响
circuitBreakerRegistry.getAllCircuitBreakers().forEach({ c -> c.reset() })
loadBalancerClientFactoryInstance = (RoundRobinWithRequestSeparatedPositionLoadBalancer) loadBalancerClientFactory.getInstance("testService")
def breaker
try {
breaker = circuitBreakerRegistry.circuitBreaker("httpbin.org:80/anything", "testService")
} catch (ConfigurationNotFoundException e) {
breaker = circuitBreakerRegistry.circuitBreaker("httpbin.org:80/anything")
}
//打开实例 3 的断路器
breaker.transitionToOpenState()
//调用 10 次
for (i in 0..<10) {
Mono<String> stringMono = webClientNamedContextFactory.getWebClient("testService")
.get().uri("/anything").retrieve()
.bodyToMono(String.class)
println(stringMono.block())
}
then:"调用至少 10 次负载均衡器且没有异常即成功"
(10.._) * loadBalancerClientFactoryInstance.getInstanceResponseByRoundRobin(*_)
when: "调用不同的路径,验证断路器在这个路径上都是关闭"
//调用 10 次
for (i in 0..<10) {
Mono<String> stringMono = webClientNamedContextFactory.getWebClient("testService")
.get().uri("/status/200").retrieve()
.bodyToMono(String.class)
println(stringMono.block())
}
then: "调用必须为正好 10 次代表没有重试,一次成功,断路器之间相互隔离"
10 * loadBalancerClientFactoryInstance.getInstanceResponseByRoundRobin(*_)
}
}
测试针对 connectTimeout 重试
对于连接超时,我们需要验证:无论是否可以重试的方法或者路径,都必须重试,因为请求并没有真的发出去。可以这样验证:设置微服务 testServiceWithCannotConnect 一个实例正常,另一个实例会连接超时,我们配置了重试 3 次,所以每次请求应该都能成功,并且随着程序运行,后面的调用不可用的实例还会被断路,照样可以成功调用。
@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 "测试针对 connectTimeout 重试"() {
given: "设置微服务 testServiceWithCannotConnect 一个实例正常,另一个实例会连接超时"
loadBalancerClientFactory.getInstance("testServiceWithCannotConnect") >> loadBalancerClientFactoryInstance
serviceInstanceListSupplier.get() >> Flux.just(Lists.newArrayList(zone1Instance1, zone1Instance2))
when:
//由于我们针对 testService 返回了两个实例,一个可以正常连接,一个不可以,但是我们配置了重试 3 次,所以每次请求应该都能成功,并且随着程序运行,后面的调用不可用的实例还会被断路
//这里主要测试针对 connect time out 还有 断路器打开的情况都会重试,并且无论是 GET 方法还是其他的
Span span = tracer.nextSpan()
for (i in 0..<10) {
Tracer.SpanInScope cleared = tracer.withSpanInScope(span)
try {
//测试 get 方法(默认 get 方法会重试)
Mono<String> stringMono = webClientNamedContextFactory.getWebClient("testServiceWithCannotConnect")
.get().uri("/anything").retrieve()
.bodyToMono(String.class)
println(stringMono.block())
//测试 post 方法(默认 post 方法针对请求已经发出的不会重试,这里没有发出请求所以还是会重试的)
stringMono = webClientNamedContextFactory.getWebClient("testServiceWithCannotConnect")
.post().uri("/anything").retrieve()
.bodyToMono(String.class)
println(stringMono.block())
}
finally {
cleared.close()
}
}
then:"调用至少 20 次负载均衡器且没有异常即成功"
(20.._) * loadBalancerClientFactoryInstance.getInstanceResponseByRoundRobin(*_)
}
}
微信搜索“我的编程喵”关注公众号,每日一刷,轻松提升技术,斩获各种offer:

SpringCloud升级之路2020.0.x版-40. spock 单元测试封装的 WebClient(上)的更多相关文章
- SpringCloud升级之路2020.0.x版-40. spock 单元测试封装的 WebClient(下)
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 我们继续上一节,继续使用 spock 测试我们自己封装的 WebClient 测试针对 r ...
- 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.微服务框架需要考虑的问题
本系列为之前系列的整理重启版,随着项目的发展以及项目中的使用,之前系列里面很多东西发生了变化,并且还有一些东西之前系列并没有提到,所以重启这个系列重新整理下,欢迎各位留言交流,谢谢!~ 上图中演示了一 ...
随机推荐
- FastAPI(62)- FastAPI 部署在 Docker
Docker 学习 https://www.cnblogs.com/poloyy/p/15257059.html 项目结构 . ├── app │ ├── __init__.py │ └── ma ...
- java/ kotlin下的单例模式
单例模式属于创建型模式, 顾名思义,就是说整个系统中只有一个该对象的实例. 为什么要使用单例模式? 1, 对于一些需要频繁创建,销毁的对象, 使用单例模式可以节省系统资源 2, 对于全局持有的对象,单 ...
- Django基础1
一,web框架的本质 web应用的本质就是一个socket的服务端.而用户的浏览器就是一个客户端,具体事例如下: import socket sk = socket.socket() sk.bind( ...
- FastAPI 学习之路(十三)Cookie 参数,Header参数
系列文章: FastAPI 学习之路(一)fastapi--高性能web开发框架 FastAPI 学习之路(二) FastAPI 学习之路(三) FastAPI 学习之路(四) FastAPI 学习之 ...
- Java(11)方法详细介绍
作者:季沐测试笔记 原文地址:https://www.cnblogs.com/testero/p/15201577.html 博客主页:https://www.cnblogs.com/testero ...
- [转载]CSS3实现文本垂直排列
最近的一个项目中要使文字垂直排列,也就是运用了CSS的writing-mode属性. writing-mode最初时ie中支持的一个属性,后来在CSS3中增添了这一新的属性,所以在ie中和其他浏览器中 ...
- 4.7 80--删除排序数组中的重复项 II
因为python的list可以直接del List[index],因此直接使用了暴力方法,判断是否重复了两次,是的话直接使用del. 在转向使用Java时,因为暴力方法的局限,一直在找怎样对Java的 ...
- 锚点布局anchorlayout在kv中的引用
from kivy.app import App from kivy.uix.anchorlayout import AnchorLayout from kivy.uix.button import ...
- Python | 实现pdf文件分页
不知道大家有没有遇到过这么一种情况,就比如一个pdf格式的电子书,我们经常浏览的是其中的一部分,而这电子书的页数很大,每当需要浏览时,就需要翻到对应的页码,就有点儿繁琐. 还有一些情况,比如,我们想分 ...
- IDA*、操作打表、并行处理-The Rotation Game HDU - 1667
万恶之源 优秀题解 用文字终究难以穷尽代码的思想 思路 每次操作都有八种选择,相当于一棵每次延申八个子节点的搜索树,故搜索应该是一种方法.而这题要求求最少步数,我们就可以想到可以试试迭代加深搜索(但其 ...