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

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

image.png



本系列代码地址: 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(*_)
  }
}
相关实践学习
部署高可用架构
本场景主要介绍如何使用云服务器ECS、负载均衡SLB、云数据库RDS和数据传输服务产品来部署多可用区高可用架构。
负载均衡入门与产品使用指南
负载均衡(Server Load Balancer)是对多台云服务器进行流量分发的负载均衡服务,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 本课程主要介绍负载均衡的相关技术以及阿里云负载均衡产品的使用方法。
相关文章
|
3月前
|
算法 测试技术 C++
【矩阵快速幂】封装类及测试用例及样例
【矩阵快速幂】封装类及测试用例及样例
原生php实现列表接口+分页接口+排序接口组合使用+包括测试数据(不加任何封装)
原生php实现列表接口+分页接口+排序接口组合使用+包括测试数据(不加任何封装)
原生php实现列表接口+分页接口+排序接口组合使用+包括测试数据(不加任何封装)
对自己的“平均随机数封装类”的测试
对自己的“平均随机数封装类”的测试
|
6月前
|
Java Nacos 开发工具
Spring Boot 单体应用一键升级成 Spring Cloud Alibaba(2)
Spring Boot 单体应用一键升级成 Spring Cloud Alibaba
86 0
Spring Boot 单体应用一键升级成 Spring Cloud Alibaba(2)
|
6月前
|
Kubernetes Java 微服务
Spring Boot 单体应用一键升级成 Spring Cloud Alibaba(1)
Spring Boot 单体应用一键升级成 Spring Cloud Alibaba(1)
90 0
Spring Boot 单体应用一键升级成 Spring Cloud Alibaba(1)
|
4月前
|
算法 测试技术 C#
【矩阵快速幂】封装类及测试用例及样例
【矩阵快速幂】封装类及测试用例及样例
|
4月前
|
测试技术
Appium自动化测试swipe滑动封装
Appium自动化测试swipe滑动封装
25 0
|
4月前
|
负载均衡 Java Nacos
Spring Boot 单体应用升级 Spring Cloud 微服务
Spring Boot 单体应用升级 Spring Cloud 微服务
137506 3
|
5月前
|
Oracle Java 关系型数据库
Generator【SpringBoot集成】代码生成+knife4j接口文档(2种模板设置、逻辑删除、字段填充 含代码粘贴可用)保姆级教程(注意事项+建表SQL+代码生成类封装+测试类)
Generator【SpringBoot集成】代码生成+knife4j接口文档(2种模板设置、逻辑删除、字段填充 含代码粘贴可用)保姆级教程(注意事项+建表SQL+代码生成类封装+测试类)
28 0

热门文章

最新文章