专栏目录
20. 启动一个 Eureka Server 集群 26.OpenFeign的组件 32. 改进负载均衡算法 27.OpenFeign的生命周期-创建代理 33. 实现重试、断路器以及线程隔离源码 8.理解 NamedContextFactory 25.OpenFeign简介与使用 14.UnderTow AccessLog 配置介绍 17.Eureka的实例配置 1. 背景 21.Spring Cloud LoadBalancer简介 37. 实现异步的客户端封装配置管理的意义与设计 44.避免链路信息丢失做的设计(2) 6.微服务特性相关的依赖说明 10.使用Log4j2以及一些核心配置 40. spock 单元测试封装的 WebClient(上) 11.Log4j2 监控相关 3.Eureka Server 与 API 网关要考虑的问题 15.UnderTow 订制 45. 实现公共日志记录 16.Eureka架构和核心概念 9.如何理解并定制一个Spring Cloud组件 12.UnderTow 简介与内部原理 38. 实现自定义 WebClient 的 NamedContextFactory 39. 改造 resilience4j 粘合 WebClient 19.Eureka的服务端设计与配置 41. SpringCloudGateway 基本流程讲解(2) 29.Spring Cloud OpenFeign 的解析(1) 42.SpringCloudGateway 现有的可供分析的请求日志以及缺陷 23.订制Spring Cloud LoadBalancer 35. 验证线程隔离正确性 40. spock 单元测试封装的 WebClient(下) 28.OpenFeign的生命周期-进行调用 7.从Bean到SpringCloud 44.避免链路信息丢失做的设计(1) 41. SpringCloudGateway 基本流程讲解(1) 24.测试Spring Cloud LoadBalancer 36. 验证断路器正确性 30. FeignClient 实现重试 5.所有项目的parent与spring-framework-common说明 22.Spring Cloud LoadBalancer核心源码 4.maven依赖回顾以及项目框架结构 18.Eureka的客户端核心设计和配置 13.UnderTow 核心配置 43.为何 SpringCloudGateway 中会有链路信息丢失 2.微服务框架需要考虑的问题 34.验证重试配置正确性 31. FeignClient 实现断路器以及线程隔离限流的思路

40. spock 单元测试封装的 WebClient(下)

unknown
• 阅读 898

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

40. spock 单元测试封装的 WebClient(下)

点赞
收藏
评论区
推荐文章

暂无数据

unknown
unknown
Lv1
男 · rrrr · rrrrrrrr
rrrrr
文章
0
粉丝
17
获赞
0
热门文章

暂无数据