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

干货满满张哈希 等级 59 0 0

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

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

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

收藏
评论区

相关推荐

Using Spock to test Java or Groovy applications
Before ====== Spock是用于groovy项目的单元测试框架,这个框架简单易用,值得推广。 Coding ====== <dependencies> <dependency> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy-all</artifact
Groovy常用编程知识点简明教程
###概述### Groovy 是一门基于JVM的动态类型语言,可以与 Java 平台几乎无缝融合(与Java代码一样编译成字节码)。 使用 Groovy ,可以增强 Java 应用的灵活性和可扩展性,提升开发效率。使用 Groovy + Spock 组合也更容易编写简洁易懂的单测。熟练使用 Groovy 编程是很有益处的。 本文针对已有一定java开发
Spring5的WebClient使用详解
前言 -- Spring5带来了新的响应式web开发框架WebFlux,同时,也引入了新的HttpClient框架WebClient。WebClient是Spring5中引入的执行 HTTP 请求的非阻塞、反应式客户端。它对同步和异步以及流方案都有很好的支持,WebClient发布后,RestTemplate将在将来版本中弃用,并且不会向前添加主要新功能。
SpringBoot与SpringCloud的版本对应详细版
大版本对应: Spring Boot Spring Cloud 1.2.x Angel版本 1.3.x Brixton版本 1.4.x stripes Camden版本 1.5.x Dalston版本、Edgware版本 2.0.x Finchley版本 2.1.x Greenwich.SR2 在实际开发过程中,我们需要更**详
SpringBoot与SpringCloud的版本对应详细版
大版本对应: Spring Boot Spring Cloud 1.2.x Angel版本 1.3.x Brixton版本 1.4.x stripes Camden版本 1.5.x Dalston版本、Edgware版本 2.0.x Finchley版本 2.1.x Greenwich.SR2 在实际开发过程中,我们需要更**详
SpringCloud demo
1.首先创建一个maven项目 ===============   这个maven项目会包含springcloud相关的项目,目录结构如下图:     本项目所有的springcloud版本为Finchley.SR2,对应的springboot的版本为2.0.7.RELEASE。     ![](https://img2018.cnblogs.com/
SpringCloud2.X(Finchley.SR2) Feign ribbion 超时配置
今天升级MyClouds的springcloud依赖到SpringCloud2.x(Finchley.SR2),发现部分耗时的操作一直在报如下错误: feign.RetryableException: Read timed out executing POST http://myclouds-admin-server \------------ 检查
SpringCloud的版本
Spring Cloud 项目目前仍然是快速迭代期,版本变化很快。这里整理一下版本相关的东西,备忘一下。 大版本 --- ### 版本号规则 Spring Cloud并没有熟悉的数字版本号,而是对应一个开发代号。 Cloud代号 Boot版本(train) Boot版本(tested) lifecycle Angle 1.2.x inco
springCloud Finchley升级记录
最近开发新项目顺便升级 Springcloud Dalston.SR5 到当前最新版 Finchley.SR1 由于 springboot1.5.10到当前最新版 spring2.0.1版本 升级修改比较大,记录一下 首先修改一下springboot cloud 版本号其他不用变 部分配置文件名称修改了 添加一下以下依赖会提示如何迁移 <
springcloud知识点总结
一.SpringCloud面试题口述 1.SpringCloud和Dubbo SpringCloud和Dubbo都是现在主流的微服务架构 SpringCloud是Apache旗下的Spring体系下的微服务解决方案 Dubbo是阿里系的分布式服务治理框架 从技术维度上,其实SpringCloud远远的超过Dubbo,Dubbo本身只是实现
SpringCloud升级之路2020.0.x版-37. 实现异步的客户端封装配置管理的意义与设计
本系列代码地址:https://github.com/JoJoTec/springcloudparent 为何需要封装异步 HTTP 客户端 WebClient对于同步的请求,我们使用 springcloudopenfeign 封装的 FeignClient,并做了额外的定制。对于异步的请求,使用的是异步 Http 客户端即 WebClient。WebCli
SpringCloud升级之路2020.0.x版-38. 实现自定义 WebClient 的 NamedContextFactory
本系列代码地址:https://github.com/JoJoTec/springcloudparent 实现 WeClient 的 NamedContextFactory我们要实现的是不同微服务自动配置装载不同的 WebClient Bean,这样就可以通过 NamedContextFactory 实现。我们先来编写下实现这个 NamedContextFa
SpringCloud升级之路2020.0.x版-39. 改造 resilience4j 粘合 WebClient
本系列代码地址:https://github.com/JoJoTec/springcloudparent要想实现我们上一节中提到的: 需要在重试以及断路中加一些日志,便于日后的优化 需要定义重试的 Exception,并且与断路器相结合,将非 2xx 的响应码也封装成特定的异常 需要在断路器相关的 Operator 中增加类似于 FeignClient
SpringCloud升级之路2020.0.x版-40. spock 单元测试封装的 WebClient(上)
本系列代码地址:https://github.com/JoJoTec/springcloudparent我们来测试下前面封装好的 WebClient,这里开始,我们使用 spock 编写 groovy 单元测试,这种编写出来的单元测试,代码更加简洁,同时更加灵活,我们在接下来的单元测试代码中就能看出来。 编写基于 spock 的 springboot con
SpringCloud升级之路2020.0.x版-40. spock 单元测试封装的 WebClient(下)
本系列代码地址:https://github.com/JoJoTec/springcloudparent我们继续上一节,继续使用 spock 测试我们自己封装的 WebClient 测试针对 readTimeout 重试针对响应超时,我们需要验证重试仅针对可以重试的方法(包括 GET 方法以及配置的可重试方法),针对不可重试的方法没有重试。我们可以通过 sp