本系列代码地址: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

最新文章

  1. java学习第15天(Linklist Vector)
  2. 排队打饭 sdut 2443【最简单的贪心法应用举例】
  3. nginx.conf文件说明
  4. Oracle Outline总结
  5. 畅谈Spring设计哲学
  6. django: startproject
  7. java与.net比较学习系列(6) 数组
  8. Cookie同域,跨域单点登录(转)
  9. Backup and Recovery Strategies1
  10. bzoj 3611[Heoi2014]大工程 虚树+dp
  11. ZXing生成条形码、二维码、带logo二维码
  12. Docker 集群
  13. SQLServer之创建DML AFTER UPDATE触发器
  14. Django form表单功能的引用(注册,复写form.clean方法 增加 验证密码功能)
  15. 阿里云配置tomcat https
  16. Javascript合并表格相同内容单元格示例
  17. Docker镜像推送(push)到Docker Hub
  18. oracle 中decode函数用法
  19. maven多环境发布.
  20. SqlServer触发器实现表的级联插入、级联更新

热门文章

  1. 重学c#系列——字典(十一)
  2. C#开发BIMFACE系列49 Web网页中加载模型与图纸的技术方案
  3. 小白自制Linux开发板 五. Debian文件系统制作,以及WIFI配置、交换分区配置
  4. 【UE4】Windows 的几种打包方式
  5. OO第三次博客作业--第三单元总结
  6. poi实现生成下拉选联动
  7. Oracle 11g 新建用户
  8. Apache Kafka 学习笔记
  9. 你知道怎么从jar包里获取一个文件的内容吗
  10. CentOS 7 tmpwatch 2.11 版本变更,移除 cronjob 任务