前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SpringCloud升级之路2020.0.x版-40. spock 单元测试封装的 WebClient(下)

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

作者头像
干货满满张哈希
发布2021-12-30 16:05:21
3070
发布2021-12-30 16:05:21
举报
文章被收录于专栏:干货满满张哈希

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

我们继续上一节,继续使用 spock 测试我们自己封装的 WebClient

测试针对 readTimeout 重试

针对响应超时,我们需要验证重试仅针对可以重试的方法(包括 GET 方法以及配置的可重试方法),针对不可重试的方法没有重试。我们可以通过 spock 单元测试中,检查对于负载均衡器获取实例方法的调用次数看出来是否有重试

我们通过 httpbin.org 的 ‘/delay/秒’ 实现 readTimeout,分别验证:

  • 测试 GET 延迟 2 秒返回,超过读取超时,这时候会重试
  • 测试 POST 延迟 3 秒返回,超过读取超时,同时路径在重试路径中,这样也是会重试的
  • 测试 POST 延迟 2 秒返回,超过读取超时,同时路径在重试路径中,这样不会重试

代码如下:

代码语言:javascript
复制
@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,这个请求路径在重试路径中,会有重试
代码语言:javascript
复制
@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(*_)
	}
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021/11/23 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 测试针对 readTimeout 重试
  • 测试非 2xx 响应码返回的重试
相关产品与服务
负载均衡
负载均衡(Cloud Load Balancer,CLB)提供安全快捷的四七层流量分发服务,访问流量经由 CLB 可以自动分配到多台后端服务器上,扩展系统的服务能力并消除单点故障。轻松应对大流量访问场景。 网关负载均衡(Gateway Load Balancer,GWLB)是运行在网络层的负载均衡。通过 GWLB 可以帮助客户部署、扩展和管理第三方虚拟设备,操作简单,安全性强。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档