SpringCloud升級之路2020.0.x版-24.測試Spring Cloud LoadBalancer

乾貨滿滿張雜湊發表於2021-08-29

本系列程式碼地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford

通過單元測試,我們也可以瞭解下一般我們實現 spring cloud 自定義的基礎元件,怎麼去單元測試。

這裡的單元測試主要測試三個場景:

  1. 只返回同一個 zone 下的例項,其他 zone 的不會返回
  2. 對於多個請求,每個請求返回的與上次的例項不同。
  3. 對於多執行緒的每個請求,如果重試,返回的都是不同的例項

同時,我們也需要針對同步和非同步兩個配置,分別進行測試,同步和非同步兩種配置測試邏輯是一樣的,只是測試的 Bean 不一樣

  • 同步環境是 DiscoveryClient,非同步環境是 ReactiveDiscoveryClient
  • 同步環境負載均衡器是 LoadBalancer,非同步環境負載均衡器是 ReactiveLoadBalancer

同步測試程式碼請參考LoadBalancerTest.java非同步測試程式碼請參考LoadBalancerTest.java

我們這裡使用同步測試程式碼作為例子展示:

//SpringExtension也包含了MockitoJUnitRunner,所以 @Mock 等註解也生效了
@ExtendWith(SpringExtension.class)
@SpringBootTest(properties = {LoadBalancerEurekaAutoConfiguration.LOADBALANCER_ZONE + "=zone1"})
public class LoadBalancerTest {

    @EnableAutoConfiguration
    @Configuration
    public static class App {
        @Bean
        public DiscoveryClient myDiscoveryClient() {
            ServiceInstance zone1Instance1 = Mockito.spy(ServiceInstance.class);
            ServiceInstance zone1Instance2 = Mockito.spy(ServiceInstance.class);
            ServiceInstance zone2Instance3 = Mockito.spy(ServiceInstance.class);
            Map<String, String> zone1 = Map.ofEntries(
                    Map.entry("zone", "zone1")
            );
            Map<String, String> zone2 = Map.ofEntries(
                    Map.entry("zone", "zone2")
            );
            when(zone1Instance1.getMetadata()).thenReturn(zone1);
            when(zone1Instance1.getInstanceId()).thenReturn("instance1");
            when(zone1Instance2.getMetadata()).thenReturn(zone1);
            when(zone1Instance2.getInstanceId()).thenReturn("instance2");
            when(zone2Instance3.getMetadata()).thenReturn(zone2);
            when(zone2Instance3.getInstanceId()).thenReturn("instance3");
            DiscoveryClient spy = Mockito.spy(DiscoveryClient.class);
            Mockito.when(spy.getInstances("testService"))
                    .thenReturn(List.of(zone1Instance1, zone1Instance2, zone2Instance3));
            return spy;
        }
    }

    @SpyBean
    private LoadBalancerClientFactory loadBalancerClientFactory;
    @SpyBean
    private Tracer tracer;

    /**
     * 只返回同一個 zone 下的例項
     */
    @Test
    public void testFilteredByZone() {
        ReactiveLoadBalancer<ServiceInstance> testService =
                loadBalancerClientFactory.getInstance("testService");
        for (int i = 0; i < 100; i++) {
            ServiceInstance server = Mono.from(testService.choose()).block().getServer();
            //必須處於和當前例項同一個zone下
            Assertions.assertEquals(server.getMetadata().get("zone"), "zone1");
        }
    }

    /**
     * 返回不同的例項
     */
    @Test
    public void testReturnNext() {
        ReactiveLoadBalancer<ServiceInstance> testService =
                loadBalancerClientFactory.getInstance("testService");
        Span span = tracer.nextSpan();
        for (int i = 0; i < 100; i++) {
            try (Tracer.SpanInScope cleared = tracer.withSpanInScope(span)) {
                ServiceInstance server1 = Mono.from(testService.choose()).block().getServer();
                ServiceInstance server2 = Mono.from(testService.choose()).block().getServer();
                //每次選擇的是不同例項
                Assertions.assertNotEquals(server1.getInstanceId(), server2.getInstanceId());
            }
        }
    }

    /**
     * 跨執行緒,預設情況下是可能返回同一例項的,在我們的實現下,保持
     * span 則會返回下一個例項,這樣保證多執行緒環境同一個 request 重試會返回下一例項
     *
     * @throws Exception
     */
    @Test
    public void testSameSpanReturnNext() throws Exception {
        Span span = tracer.nextSpan();
        for (int i = 0; i < 100; i++) {
            try (Tracer.SpanInScope cleared = tracer.withSpanInScope(span)) {
                ReactiveLoadBalancer<ServiceInstance> testService =
                        loadBalancerClientFactory.getInstance("testService");
                ServiceInstance server1 = Mono.from(testService.choose()).block().getServer();
                AtomicReference<ServiceInstance> server2 = new AtomicReference<>();
                Thread thread = new Thread(() -> {
                    try (Tracer.SpanInScope cleared2 = tracer.withSpanInScope(span)) {
                        server2.set(Mono.from(testService.choose()).block().getServer());
                    }
                });
                thread.start();
                thread.join();
                System.out.println(i);
                Assertions.assertNotEquals(server1.getInstanceId(), server2.get().getInstanceId());
            }
        }
    }
}

執行測試,測試通過。

我們這一節使用單元測試驗證我們要實現的這些功能是否有效。下一節,我們將開始分析同步環境下的 Http 客戶端,Open-Feign Client。

微信搜尋“我的程式設計喵”關注公眾號,每日一刷,輕鬆提升技術,斬獲各種offer

相關文章