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

本文涉及的产品
传统型负载均衡 CLB,每月750个小时 15LCU
应用型负载均衡 ALB,每月750个小时 15LCU
网络型负载均衡 NLB,每月750个小时 15LCU
简介: SpringCloud升级之路2020.0.x版-40. spock 单元测试封装的 WebClient(下)

image.png


本系列代码地址: 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(*_)
  }
}
相关实践学习
SLB负载均衡实践
本场景通过使用阿里云负载均衡 SLB 以及对负载均衡 SLB 后端服务器 ECS 的权重进行修改,快速解决服务器响应速度慢的问题
负载均衡入门与产品使用指南
负载均衡(Server Load Balancer)是对多台云服务器进行流量分发的负载均衡服务,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 本课程主要介绍负载均衡的相关技术以及阿里云负载均衡产品的使用方法。
相关文章
|
8月前
|
消息中间件 Java 测试技术
Spring Cloud Alibaba环境问题之测试环境失败如何解决
Spring Cloud Alibaba提供了一套在Spring Cloud框架基础上构建的微服务解决方案,旨在简化分布式系统的开发和管理;本合集将探讨Spring Cloud Alibaba在实际应用中的部署和使用技巧,以及该框架常见问题的诊断方法和解决步骤。
|
8月前
|
缓存 负载均衡 Java
Spring Cloud Alibaba client升级问题之升级报错如何解决
Spring Cloud Alibaba提供了一套在Spring Cloud框架基础上构建的微服务解决方案,旨在简化分布式系统的开发和管理;本合集将探讨Spring Cloud Alibaba在实际应用中的部署和使用技巧,以及该框架常见问题的诊断方法和解决步骤。
|
Java Nacos 开发工具
Spring Boot 单体应用一键升级成 Spring Cloud Alibaba(2)
Spring Boot 单体应用一键升级成 Spring Cloud Alibaba
152 0
Spring Boot 单体应用一键升级成 Spring Cloud Alibaba(2)
|
2月前
|
JSON Java 测试技术
SpringCloud2023实战之接口服务测试工具SpringBootTest
SpringBootTest同时集成了JUnit Jupiter、AssertJ、Hamcrest测试辅助库,使得更容易编写但愿测试代码。
72 3
|
Kubernetes Java 微服务
Spring Boot 单体应用一键升级成 Spring Cloud Alibaba(1)
Spring Boot 单体应用一键升级成 Spring Cloud Alibaba(1)
157 0
Spring Boot 单体应用一键升级成 Spring Cloud Alibaba(1)
|
3月前
|
存储 消息中间件 运维
架构升级的救星!流量回放自动化测试的必备指南
大家好,我是小米,一名29岁的技术宅。今天分享一个物联网领域的实用技能——流量回放自动化测试。系统重构后,测试工作量巨大,本文介绍如何通过日志收集和数据回放进行自动化测试,包括离线、实时和并行回放模式,帮助快速定位Bug,提升测试效率和系统稳定性。欢迎关注我的微信公众号“软件求生”,获取更多技术干货!
76 3
|
5月前
|
算法
测试工程师的技能升级:LeetCode算法挑战与职业成长
这篇文章通过作者亲身体验LeetCode算法题的过程,探讨了测试工程师学习算法的重要性,并强调了算法技能对于测试职业成长的必要性。
91 1
测试工程师的技能升级:LeetCode算法挑战与职业成长
|
6月前
|
关系型数据库 MySQL 测试技术
数据库升级是一个涉及数据备份、新版本安装、数据迁移和测试等关键环节的复杂过程
【7月更文挑战第21天】数据库升级是一个涉及数据备份、新版本安装、数据迁移和测试等关键环节的复杂过程
154 1
|
5月前
|
缓存 Java Maven
SpringCloud基于Eureka的服务治理架构搭建与测试:从服务提供者到消费者的完整流程
Spring Cloud微服务框架中的Eureka是一个用于服务发现和注册的基础组件,它基于RESTful风格,为微服务架构提供了关键的服务注册与发现功能。以下是对Eureka的详细解析和搭建举例。
|
7月前
|
Java 测试技术 持续交付
Java一分钟之-Spring Cloud Contract:契约测试
【6月更文挑战第16天】Spring Cloud Contract是微服务契约测试框架,通过DSL定义接口行为,使用WireMock生成存根进行独立开发验证。常见问题包括契约编写不清晰、未集成到CI/CD和契约版本控制混乱。例如,定义一个`GET /greeting`返回JSON响应的契约,Spring Cloud Contract会自动生成测试代码,帮助确保服务间接口一致性,提升开发效率和系统稳定性。
149 7