专栏目录
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(上)

干货满满张哈希
• 阅读 261

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

本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent

我们来测试下前面封装好的 WebClient,这里开始,我们使用 spock 编写 groovy 单元测试,这种编写出来的单元测试,代码更加简洁,同时更加灵活,我们在接下来的单元测试代码中就能看出来。

编写基于 spock 的 spring-boot context 测试

我们加入前面设计的配置,编写测试类:

@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 {
    }
}

我们加入三个服务实例供单元测试调用:

class WebClientUnitTest extends Specification {
    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")))
}

我们要动态的指定负载均衡获取服务实例列表的响应,即去 Mock 负载均衡器的 ServiceInstanceListSupplier 并覆盖:

class WebClientUnitTest extends Specification {

    @Autowired
    private Tracer tracer
    @Autowired
    private ServiceInstanceMetrics serviceInstanceMetrics

    RoundRobinWithRequestSeparatedPositionLoadBalancer loadBalancerClientFactoryInstance = Spy();
    ServiceInstanceListSupplier serviceInstanceListSupplier = Spy();

    //所有测试的方法执行前会调用的方法
    def setup() {
        //初始化 loadBalancerClientFactoryInstance 负载均衡器
        loadBalancerClientFactoryInstance.setTracer(tracer)
        loadBalancerClientFactoryInstance.setServiceInstanceMetrics(serviceInstanceMetrics)
        loadBalancerClientFactoryInstance.setServiceInstanceListSupplier(serviceInstanceListSupplier)
    }
}

之后,我们可以通过下面的 groovy 代码,动态指定微服务返回实例:

//指定 testService 微服务的 LoadBalancer 为 loadBalancerClientFactoryInstance
loadBalancerClientFactory.getInstance("testService") >> loadBalancerClientFactoryInstance
//指定 testService 微服务实例列表为 zone1Instance1, zone1Instance3
serviceInstanceListSupplier.get() >> Flux.just(Lists.newArrayList(zone1Instance1, zone1Instance3))

测试断路器异常重试以及断路器级别

我们需要验证:

  • 对于断路器打开的异常,由于没有请求发出去,所以需要直接重试其他的实例。我们可以设立一个微服务,包含两个实例,将其中一个实例的某个路径断路器打开,之后多次调用这个微服务的这个路径接口,看是否都调用成功(由于有重试,所以每次调用都会成功)。同时验证,对于负载均衡器获取服务实例的调用,多于调用次数(每次重试都会调用负载均衡器获取一个新的实例用于调用)
  • 某个路径断路器打开的时候,其他路径断路器不会打开。在上面打开一个微服务某个实例的一个路径的断路器之后,我们调用其他的路径,无论多少次,都成功并且调用负载均衡器获取服务实例的次数等于调用次数,代表没有重试,也就是没有断路器异常

编写代码:

@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 "测试断路器异常重试以及断路器级别"() {
        given: "设置 testService 的实例都是正常实例"
            loadBalancerClientFactory.getInstance("testService") >> loadBalancerClientFactoryInstance
            serviceInstanceListSupplier.get() >> Flux.just(Lists.newArrayList(zone1Instance1, zone1Instance3))
        when: "断路器打开"
            //清除断路器影响
            circuitBreakerRegistry.getAllCircuitBreakers().forEach({ c -> c.reset() })
            loadBalancerClientFactoryInstance = (RoundRobinWithRequestSeparatedPositionLoadBalancer) loadBalancerClientFactory.getInstance("testService")
            def breaker
            try {
                breaker = circuitBreakerRegistry.circuitBreaker("httpbin.org:80/anything", "testService")
            } catch (ConfigurationNotFoundException e) {
                breaker = circuitBreakerRegistry.circuitBreaker("httpbin.org:80/anything")
            }
            //打开实例 3 的断路器
            breaker.transitionToOpenState()
            //调用 10 次
            for (i in 0..<10) {
                Mono<String> stringMono = webClientNamedContextFactory.getWebClient("testService")
                                                                      .get().uri("/anything").retrieve()
                                                                      .bodyToMono(String.class)
                println(stringMono.block())
            }
        then:"调用至少 10 次负载均衡器且没有异常即成功"
            (10.._) * loadBalancerClientFactoryInstance.getInstanceResponseByRoundRobin(*_)
        when: "调用不同的路径,验证断路器在这个路径上都是关闭"
            //调用 10 次
            for (i in 0..<10) {
                Mono<String> stringMono = webClientNamedContextFactory.getWebClient("testService")
                                                                      .get().uri("/status/200").retrieve()
                                                                      .bodyToMono(String.class)
                println(stringMono.block())
            }
        then: "调用必须为正好 10 次代表没有重试,一次成功,断路器之间相互隔离"
            10 * loadBalancerClientFactoryInstance.getInstanceResponseByRoundRobin(*_)
    }
}

测试针对 connectTimeout 重试

对于连接超时,我们需要验证:无论是否可以重试的方法或者路径,都必须重试,因为请求并没有真的发出去。可以这样验证:设置微服务 testServiceWithCannotConnect 一个实例正常,另一个实例会连接超时,我们配置了重试 3 次,所以每次请求应该都能成功,并且随着程序运行,后面的调用不可用的实例还会被断路,照样可以成功调用。

@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 "测试针对 connectTimeout 重试"() {
        given: "设置微服务 testServiceWithCannotConnect 一个实例正常,另一个实例会连接超时"
            loadBalancerClientFactory.getInstance("testServiceWithCannotConnect") >> loadBalancerClientFactoryInstance
            serviceInstanceListSupplier.get() >> Flux.just(Lists.newArrayList(zone1Instance1, zone1Instance2))
        when:
            //由于我们针对 testService 返回了两个实例,一个可以正常连接,一个不可以,但是我们配置了重试 3 次,所以每次请求应该都能成功,并且随着程序运行,后面的调用不可用的实例还会被断路
            //这里主要测试针对 connect time out 还有 断路器打开的情况都会重试,并且无论是 GET 方法还是其他的
            Span span = tracer.nextSpan()
            for (i in 0..<10) {
                Tracer.SpanInScope cleared = tracer.withSpanInScope(span)
                try {
                    //测试 get 方法(默认 get 方法会重试)
                    Mono<String> stringMono = webClientNamedContextFactory.getWebClient("testServiceWithCannotConnect")
                                                                          .get().uri("/anything").retrieve()
                                                                          .bodyToMono(String.class)
                    println(stringMono.block())
                    //测试 post 方法(默认 post 方法针对请求已经发出的不会重试,这里没有发出请求所以还是会重试的)
                    stringMono = webClientNamedContextFactory.getWebClient("testServiceWithCannotConnect")
                                                             .post().uri("/anything").retrieve()
                                                             .bodyToMono(String.class)
                    println(stringMono.block())
                }
                finally {
                    cleared.close()
                }
            }
        then:"调用至少 20 次负载均衡器且没有异常即成功"
            (20.._) * loadBalancerClientFactoryInstance.getInstanceResponseByRoundRobin(*_)
    }
}

微信搜索“我的编程喵”关注公众号,每日一刷,轻松提升技术,斩获各种offer

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

评论区
推荐文章

暂无数据