This is the 22nd day of my participation in the November Gwen Challenge. Check out the event details: The last Gwen Challenge 2021
This series code address: github.com/JoJoTec/spr…
Let’s test the WebClient package. Starting with Spock, we’ll write groovy unit tests. The resulting unit tests are much simpler and more flexible, as we’ll see in the following unit test code.
Write spring-Boot Context tests based on Spock
We add the configuration we designed earlier and write the test class:
@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", / / for retry is three times, in order to prevent the breaker on impact test, the number of proportion is set to just try one more time, Prevent trigger / / at the same time, we also need to manually clear circuit breaker when test statistics ". Resilience4j circuitbreaker. Configs. Default. MinimumNumberOfCalls = 4 ", "resilience4j.circuitbreaker.configs.default.recordExceptions=java.lang.Exception" ], classes = MockConfig ) class WebClientUnitTest extends Specification { @SpringBootApplication static class MockConfig { }}Copy the code
We added three service instances for unit test invocation:
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")))
}
Copy the code
We get the service instance list to dynamic load balancing of the response, namely to Mock the load balancer ServiceInstanceListSupplier and covers:
class WebClientUnitTest extends Specification { @Autowired private Tracer tracer @Autowired private ServiceInstanceMetrics serviceInstanceMetrics RoundRobinWithRequestSeparatedPositionLoadBalancer loadBalancerClientFactoryInstance = Spy(); ServiceInstanceListSupplier serviceInstanceListSupplier = Spy(); / / in front of all the test method will be called the method of def setup () {/ / initialization loadBalancerClientFactoryInstance load balancer loadBalancerClientFactoryInstance.setTracer(tracer) loadBalancerClientFactoryInstance.setServiceInstanceMetrics(serviceInstanceMetrics) loadBalancerClientFactoryInstance.setServiceInstanceListSupplier(serviceInstanceListSupplier) } }Copy the code
We can then dynamically specify a microservice return instance using groovy code like this:
/ / specified LoadBalancer as loadBalancerClientFactoryInstance testService micro service LoadBalancerClientFactory. GetInstance (" testService ") > > loadBalancerClientFactoryInstance / / testService micro service instance list is specified zone1Instance1, zone1Instance3 serviceInstanceListSupplier.get() >> Flux.just(Lists.newArrayList(zone1Instance1, zone1Instance3))Copy the code
Test circuit breaker abnormal retry and circuit breaker level
We need to verify:
- For the breaker open exception, the other instances need to be retried directly because no request was sent. We can set up a microservice with two instances, turn on a path breaker in one instance, and then call the path interface of the microservice several times to see if both calls succeed (each call will succeed because of the retry). Also verify that the load balancer gets a service instance more than the number of calls (each retry will call the load balancer to get a new instance for the call)
- When a path circuit breaker is on, other path circuit breakers are not on. After opening a circuit breaker for one path of a microservice instance above, we call the other path, no matter how many times, successfully and call the load balancer to get the service instance equal to the number of calls, representing no retries, i.e., no circuit breaker exception.
Write code:
@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", / / for retry is three times, in order to prevent the breaker on impact test, the number of proportion is set to just try one more time, Prevent trigger / / at the same time, we also need to manually clear circuit breaker when test statistics ". 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(); / / in front of all the test method will be called the method of def setup () {/ / initialization loadBalancerClientFactoryInstance load balancer loadBalancerClientFactoryInstance.setTracer(tracer) loadBalancerClientFactoryInstance.setServiceInstanceMetrics(serviceInstanceMetrics) LoadBalancerClientFactoryInstance. SetServiceInstanceListSupplier (serviceInstanceListSupplier)} def "abnormal retry test circuit breaker and the breaker level" () { given: "Set" testService instances are normal instance loadBalancerClientFactory. GetInstance (" testService ") > > loadBalancerClientFactoryInstance serviceInstanceListSupplier.get() >> Flux.just(Lists.newArrayList(zone1Instance1, zone1Instance3)) when: "Open circuit breaker" / / removal of circuit breaker affect circuitBreakerRegistry getAllCircuitBreakers (). The forEach ({c - > c.r eset ()}) 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 of the breaker breaker. Open the instance transitionToOpenState () / / calls 10 For (I in 0.. <10) { Mono<String> stringMono = webClientNamedContextFactory.getWebClient("testService") .get().uri("/anything").retrieve().bodytomono (string.class) println(stringmono.block ())} then:" Successful calls to the load balancer at least 10 times without exceptions" (10.. _) * loadBalancerClientFactoryInstance.getInstanceResponseByRoundRobin(*_) when: "Call a different path to verify that the circuit breaker is closed on that path" // Call for (I in 0.. <10) { Mono<String> stringMono = webClientNamedContextFactory.getWebClient("testService") .get().uri("/status/200").retrieve() .bodyToMono(String.class) println(stringMono.block()) } then: "Call must be for just 10 times represent no retry, a successful, isolation between circuit breaker" 10. * loadBalancerClientFactoryInstance getInstanceResponseByRoundRobin (* _)}}Copy the code
Test retries for connectTimeout
For connection timeouts, we need validation: regardless of whether the method or path can be retried, it must be retried because the request was not actually sent. Micro services can be set so that validation: testServiceWithCannotConnect an instance is normal, another instance can connection timeout, we configure the retry 3 times, so every time the request should be successfully, and the program is running, the back of the call, not with the instance will be break, still can be a successful call.
@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", / / for retry is three times, in order to prevent the breaker on impact test, the number of proportion is set to just try one more time, Prevent trigger / / at the same time, we also need to manually clear circuit breaker when test statistics ". 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(); / / in front of all the test method will be called the method of def setup () {/ / initialization loadBalancerClientFactoryInstance load balancer loadBalancerClientFactoryInstance.setTracer(tracer) loadBalancerClientFactoryInstance.setServiceInstanceMetrics(serviceInstanceMetrics) LoadBalancerClientFactoryInstance. SetServiceInstanceListSupplier (serviceInstanceListSupplier)} def "test for connectTimeout Retry "() {given: "Set the micro testServiceWithCannotConnect service an instance is normal, Another example would be connection timeout "loadBalancerClientFactory getInstance (" testServiceWithCannotConnect") > > loadBalancerClientFactoryInstance serviceInstanceListSupplier.get() >> Flux.just(Lists.newArrayList(zone1Instance1, zone1Instance2)) when: // Since we returned two instances of testService, one working and one not, but we configured three retries, each request should be successful, and as the program runs, // The main test will retry for connect time out and breaker open cases, NextSpan = tracer.nextspan () for (I in 0.. <10) {tracer.spaninscope cleared = tracer.withspaninscope (span) try {// Test get method (default get method retry) Mono<String> stringMono = webClientNamedContextFactory.getWebClient("testServiceWithCannotConnect") .get().uri("/anything").retrieve() .bodytomono (string.class) println(stringmono.block ()) There is no request. So will retry) stringMono = webClientNamedContextFactory getWebClient (" testServiceWithCannotConnect ") .post().uri("/anything").retrieve() .bodyToMono(String.class) println(stringMono.block()) } finally { cleared.close() } } then:" Call the load balancer at least 20 times with no exceptions is successful "(20.. _) * loadBalancerClientFactoryInstance.getInstanceResponseByRoundRobin(*_) } }Copy the code
Wechat search “my programming meow” public account, a daily brush, easy to improve skills, won a variety of offers: