SpringCloud升级之路2020.0.x版-40. spock 单元测试封装的 WebClient(下)

【SpringCloud升级之路2020.0.x版-40. spock 单元测试封装的 WebClient(下)】SpringCloud升级之路2020.0.x版-40. spock 单元测试封装的 WebClient(下)
文章图片

本系列代码地址: 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() inReadTimeoutException) { //读取超时忽略 } 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() inReadTimeoutException) { //读取超时忽略 } 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() inReadTimeoutException) { //读取超时忽略 } 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:
SpringCloud升级之路2020.0.x版-40. spock 单元测试封装的 WebClient(下)
文章图片

    推荐阅读