本系列代码地址:https://github.com/JoJoTec/sp...

咱们持续上一节,持续应用 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