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

我们来测试下前面封装好的 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(上)的更多相关文章

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

    本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 我们继续上一节,继续使用 spock 测试我们自己封装的 WebClient 测试针对 r ...

  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. electron-builder进行DEBUG输出的正确方式

    前言 使用Electron进行打包通常会用到electron-builder或者electron-packager两种工具.在使用electron-builder的时候,由于对机制的不熟悉,我们在打包 ...

  2. Python爬取 | 王者荣耀英雄皮肤海报

    这里只展示代码,具体介绍请点击下方链接. Python爬取 | 王者荣耀英雄皮肤海报 import requests import re import os import time import wi ...

  3. poj1248 (线性筛欧拉函数)(原根)

    强烈鸣谢wddwjlss 题目大意:给出一个奇素数,求出他的原根的个数,多组数据. 这里先介绍一些基本性质 阶 设\((a,m)=1\),满足\(a^r \equiv 1 \pmod m\)的最小正整 ...

  4. bzoj1972 SDOI2010-----猪国杀(模拟)

    题目自己去找吧 记得数据范围是<=10 注意事项: 1.牌库空的时候,要不断的抽第一张牌 2.反贼的决斗永远是向主公发的 3.每次判定无懈的时候,都是从使用锦囊的那个牌开始,记得敌意和殷勤的判断 ...

  5. LinkedList-常用方法以及双向链表的理解

    链表 链表是一种物理存储单元上非连续.非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的. 链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成.每个结点包括两 ...

  6. Netty 了解

    1.1 Netty 是什么? Netty is an asynchronous event-driven network application framework for rapid develop ...

  7. 半天撸一个简易版mybatis

    为什么需要持久层框架? 首先我们先看看使用原生jdbc存在的问题? public static void main(String[] args) { Connection connection = n ...

  8. 【Spring】重新认识 IoC

    前言 IoC (Inversion of control) 并不是Spring特有的概念. IoC 维基百科的解释: In software engineering, inversion of con ...

  9. vue3.x全局$toast、$message、$loading等js插件

    有时候我们需要使用一些类似toast,messge.loading这些跟js交互很频繁的插件,vue3.x这类插件的定义跟vue2.x插件稍大,而且相对变得复杂了一点点. 第一种.需要时创建,用完移除 ...

  10. elasticsearch地理位置查询

    elasticsearch地理位置查询 一.背景 二.geo数据类型 1.geo_point 2.geo_shape 三.此处对geo_point类型实战 1.背景 2.插入地点数据 1.创建索引 2 ...