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

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

干货满满张哈希
• 阅读 260

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(下)

评论区
推荐文章

暂无数据