SpringCloud升级之路2020.0.x版-40. spock 单元测试封装的 WebClient(上)
2021-11-22 09:38
375 查看
本系列代码地址: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:
相关文章推荐
- SpringCloud升级之路2020.0.x版-40. spock 单元测试封装的 WebClient(下)
- SpringCloud升级之路2020.0.x版-20. 启动一个 Eureka Server 集群
- SpringCloud升级之路2020.0.x版-34.验证重试配置正确性(2)
- SpringCloud升级之路2020.0.x版-26.OpenFeign的组件
- SpringCloud升级之路2020.0.x版-23.订制Spring Cloud LoadBalancer
- Spring Cloud 升级之路 - 2020.0.x - 1. 背景知识、需求描述与公共依赖
- SpringCloud升级之路2020.0.x版-25.OpenFeign简介与使用
- SpringCloud升级之路2020.0.x版-27.OpenFeign的生命周期-创建代理
- Spring Cloud 升级之路 - 2020.0.x - 1. 背景知识、需求描述与公共依赖
- Spring Cloud 升级之路 - 2020.0.x - 7. 使用 Spring Cloud LoadBalancer (2)
- SpringCloud升级之路2020.0.x版-21.Spring Cloud LoadBalancer简介
- SpringCloud升级之路2020.0.x版-28.OpenFeign的生命周期-进行调用
- SpringCloud升级之路2020.0.x版-22.Spring Cloud LoadBalancer核心源码
- Spring Cloud 升级之路 - 2020.0.x - 2. 使用 Undertow 作为我们
- SpringCloud升级之路2020.0.x版-29.Spring Cloud OpenFeign 的解析(1)
- Spring Cloud 升级之路 - 2020.0.x - 4. 使用 Eureka 作为注册中心
- SpringCloud升级之路2020.0.x版-18.Eureka的客户端核心设计和配置
- SpringCloud升级之路2020.0.x版-24.测试Spring Cloud LoadBalancer
- Spring Cloud 升级之路 - 2020.0.x - 4. 使用 Eureka 作为注册中心
- SpringCloud升级之路2020.0.x版-29.Spring Cloud OpenFeign 的解析(2)