美文网首页
SpringCloud升级之路2020.0.x版-40. spo

SpringCloud升级之路2020.0.x版-40. spo

作者: 干货满满张哈希 | 来源:发表于2021-11-23 09:35 被阅读0次
    image

    本系列代码地址: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(*_)
        }
    }
    

    相关文章

      网友评论

          本文标题:SpringCloud升级之路2020.0.x版-40. spo

          本文链接:https://www.haomeiwen.com/subject/bntwtrtx.html