This is an automated email from the ASF dual-hosted git repository. jimin pushed a commit to branch 2.x in repository https://gitbox.apache.org/repos/asf/incubator-seata.git
The following commit(s) were added to refs/heads/2.x by this push: new 517f58dbfd test: add mock test for seata-discovery-etcd3 (#7233) 517f58dbfd is described below commit 517f58dbfd100a893a55edcfe941b0fcc2791734 Author: hokkine <145577741+hokk...@users.noreply.github.com> AuthorDate: Mon Mar 24 11:11:57 2025 +0800 test: add mock test for seata-discovery-etcd3 (#7233) --- changes/en-us/2.x.md | 2 +- changes/zh-cn/2.x.md | 2 +- .../etcd/EtcdRegistryServiceImplMockTest.java | 241 +++++++++++++++++++++ 3 files changed, 243 insertions(+), 2 deletions(-) diff --git a/changes/en-us/2.x.md b/changes/en-us/2.x.md index e5ae453aba..7aa3d248b9 100644 --- a/changes/en-us/2.x.md +++ b/changes/en-us/2.x.md @@ -100,7 +100,7 @@ Add changes here for all PR submitted to the 2.x branch. - [[#7203](https://github.com/apache/incubator-seata/pull/7203)] Refactored tests in rm.datasource.sql.Druid and seata-sqlparser-druid module - [[#7221](https://github.com/apache/incubator-seata/pull/7221)] add UT for gRPC Encoder/Decode - [[#7227](https://github.com/apache/incubator-seata/pull/7227)] add mock test for seata-discovery-consul module - +- [[#7233][https://github.com/apache/incubator-seata/pull/7233]] add mock test for seata-discovery-etcd3 ### refactor: diff --git a/changes/zh-cn/2.x.md b/changes/zh-cn/2.x.md index 540736187b..d4fef21133 100644 --- a/changes/zh-cn/2.x.md +++ b/changes/zh-cn/2.x.md @@ -100,7 +100,7 @@ - [[#7203](https://github.com/apache/incubator-seata/pull/7203)] 重构了 rm.datasource.sql.Druid 和 seata-sqlparser-druid 模块中的测试 - [[#7221](https://github.com/apache/incubator-seata/pull/7221)] 增加 gRPC Encoder/Decoder的测试用例 - [[#7227](https://github.com/apache/incubator-seata/pull/7227)] 为 seata-discovery-consul 增加mock测试 - +- [[#7233][https://github.com/apache/incubator-seata/pull/7233]] 增加对 seata-discovery-etcd3 的mock测试 ### refactor: - [[#7145](https://github.com/apache/incubator-seata/pull/7145)] 重构不满足 license 要求的代码 diff --git a/discovery/seata-discovery-etcd3/src/test/java/org/apache/seata/discovery/registry/etcd/EtcdRegistryServiceImplMockTest.java b/discovery/seata-discovery-etcd3/src/test/java/org/apache/seata/discovery/registry/etcd/EtcdRegistryServiceImplMockTest.java new file mode 100644 index 0000000000..f70de7609f --- /dev/null +++ b/discovery/seata-discovery-etcd3/src/test/java/org/apache/seata/discovery/registry/etcd/EtcdRegistryServiceImplMockTest.java @@ -0,0 +1,241 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.discovery.registry.etcd; + +import io.etcd.jetcd.Client; +import io.etcd.jetcd.KV; +import io.etcd.jetcd.Lease; +import io.etcd.jetcd.Watch; +import io.etcd.jetcd.ByteSequence; +import io.etcd.jetcd.KeyValue; +import io.etcd.jetcd.api.RangeResponse; +import io.etcd.jetcd.api.ResponseHeader; +import io.etcd.jetcd.kv.GetResponse; +import io.etcd.jetcd.lease.LeaseGrantResponse; +import io.etcd.jetcd.lease.LeaseKeepAliveResponse; +import io.etcd.jetcd.lease.LeaseTimeToLiveResponse; + +import io.etcd.jetcd.options.GetOption; +import io.etcd.jetcd.options.PutOption; +import io.etcd.jetcd.options.WatchOption; +import org.apache.seata.config.Configuration; +import org.apache.seata.config.ConfigurationFactory; +import org.apache.seata.config.exception.ConfigNotFoundException; +import org.apache.seata.discovery.registry.etcd3.EtcdRegistryProvider; +import org.apache.seata.discovery.registry.etcd3.EtcdRegistryServiceImpl; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.BeforeEach; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.seata.common.DefaultValues.DEFAULT_TX_GROUP; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.verify; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class EtcdRegistryServiceImplMockTest { + + @InjectMocks + private EtcdRegistryServiceImpl registryService; + @Mock + private Client mockClient; + @Mock + private KV mockKVClient; + @Mock + private Lease mockLeaseClient; + @Mock + private Watch mockWatchClient; + @Mock + private Watch.Watcher mockWatcher; + @Mock + Configuration configuration; + + ExecutorService executorService; + + private final static String HOST = "127.0.0.1"; + private final static int PORT = 8091; + private static final String CLUSTER_NAME = "default"; + + @BeforeEach + public void setUp() throws NoSuchFieldException, IllegalAccessException { + MockitoAnnotations.openMocks(this); + registryService = (EtcdRegistryServiceImpl) spy(new EtcdRegistryProvider().provide()); + // mock client + when(mockClient.getLeaseClient()).thenReturn(mockLeaseClient); + when(mockClient.getWatchClient()).thenReturn(mockWatchClient); + when(mockClient.getKVClient()).thenReturn(mockKVClient); + // inject spy executorService + Field executorServiceField = EtcdRegistryServiceImpl.class.getDeclaredField("executorService"); + executorServiceField.setAccessible(true); + executorService = spy((ExecutorService) executorServiceField.get(registryService)); + executorServiceField.set(registryService, executorService); + // inject mock client + Field clientField = EtcdRegistryServiceImpl.class.getDeclaredField("client"); + clientField.setAccessible(true); + clientField.set(registryService, mockClient); + } + + @BeforeAll + public static void beforeClass() { + String endPoint = String.format("http://%s:%s", HOST, PORT); + System.setProperty(EtcdRegistryServiceImpl.TEST_ENDPONT, endPoint); + } + + @AfterAll + public static void afterClass() { + System.setProperty(EtcdRegistryServiceImpl.TEST_ENDPONT, ""); + } + + @Test + public void testRegister() throws Exception { + long leaseId = 1L; + InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8091); + // Mock lease grant response + LeaseGrantResponse leaseGrantResponse = mock(LeaseGrantResponse.class); + when(leaseGrantResponse.getID()).thenReturn(leaseId); + when(mockLeaseClient.grant(anyLong())).thenReturn(CompletableFuture.completedFuture(leaseGrantResponse)); + // Mock put response + when(mockKVClient.put(any(), any(), any(PutOption.class))).thenReturn(CompletableFuture.completedFuture(null)); + // timeToLive response + io.etcd.jetcd.api.LeaseTimeToLiveResponse timeToLiveResponseApi = + io.etcd.jetcd.api.LeaseTimeToLiveResponse.newBuilder() + .setID(leaseId) + .setTTL(6) + .build(); + when(mockLeaseClient.timeToLive(eq(leaseId), any())) + .thenReturn(CompletableFuture.completedFuture(new LeaseTimeToLiveResponse(timeToLiveResponseApi))); + // keepAlive response + io.etcd.jetcd.api.LeaseKeepAliveResponse leaseKeepAliveResponse = io.etcd.jetcd.api.LeaseKeepAliveResponse.newBuilder().build(); + when(mockLeaseClient.keepAliveOnce(eq(leaseId))) + .thenReturn(CompletableFuture.completedFuture(new LeaseKeepAliveResponse(leaseKeepAliveResponse))); + // Act + registryService.register(address); + // verify the method to register the new service is called + verify(mockKVClient, times(1)).put(any(), any(), any(PutOption.class)); + // verify lifeKeeper task is submitted + verify(executorService, times(1)).submit(any(Callable.class)); + } + + @Test + public void testUnregister() throws Exception { + InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8091); + // Mock delete response + when(mockKVClient.delete(any())).thenReturn(CompletableFuture.completedFuture(null)); + // Act + registryService.unregister(address); + // Verify + verify(mockKVClient, times(1)).delete(any()); + } + + @Test + @Order(1) + public void testLookup() throws Exception { + List<String> services = Arrays.asList("127.0.0.1:8091", "127.0.0.1:8092", "127.0.0.1:8093"); + GetResponse mockGetResponse = createMockGetResponse(services); + when(mockKVClient.get(any(ByteSequence.class), any(GetOption.class))).thenReturn(CompletableFuture.completedFuture(mockGetResponse)); + + try (MockedStatic<ConfigurationFactory> mockConfig = Mockito.mockStatic(ConfigurationFactory.class)) { + // 1. run success case + mockConfig.when(ConfigurationFactory::getInstance).thenReturn(configuration); + when(configuration.getConfig("service.vgroupMapping.default_tx_group")).thenReturn(CLUSTER_NAME); + List<InetSocketAddress> lookup = registryService.lookup(DEFAULT_TX_GROUP); + List<String> lookupServices = lookup.stream() + .map(address -> address.getHostString() + ":" + address.getPort()) + .collect(Collectors.toList()); + // assert + assertEquals(lookupServices, services); + + // 2. config not found case + when(configuration.getConfig(any())).thenReturn(null); + Assertions.assertThrows(ConfigNotFoundException.class, () -> { + registryService.lookup(DEFAULT_TX_GROUP); + }); + } + } + + private GetResponse createMockGetResponse(List<String> addresses) { + // Create mock ResponseHeader + ResponseHeader mockHeader = ResponseHeader.newBuilder().setRevision(12345L).build(); + // Create mock KeyValue list + List<KeyValue> mockKeyValues = addresses.stream() + .map(address -> { + KeyValue mockKeyValue = mock(KeyValue.class); + when(mockKeyValue.getValue()).thenReturn(ByteSequence.from(address, UTF_8)); + return mockKeyValue; + }) + .collect(Collectors.toList()); + // Create mock RangeResponse + RangeResponse mockRangeResponse = RangeResponse.newBuilder().setHeader(mockHeader).build(); + // Create mock GetResponse + GetResponse mockGetResponse = spy(new GetResponse(mockRangeResponse, ByteSequence.EMPTY)); + when(mockGetResponse.getKvs()).thenReturn(mockKeyValues); + return mockGetResponse; + } + + + @Test + public void testSubscribe() throws Exception { + Watch.Listener mockListener = mock(Watch.Listener.class); + registryService.subscribe(CLUSTER_NAME, mockListener); + //verify watcher task is submitted + verify(executorService, times(1)).submit(any(Runnable.class)); + } + + @Test + public void testUnsubscribe() throws Exception { + Watch.Listener mockListener = mock(Watch.Listener.class); + CountDownLatch latch = new CountDownLatch(1); + when(mockWatchClient.watch(any(), any(WatchOption.class), any(Watch.Listener.class))) + .thenAnswer(invocation -> { + latch.countDown(); + return mockWatcher; + }); + registryService.subscribe(DEFAULT_TX_GROUP, mockListener); + latch.await(1, TimeUnit.SECONDS); + registryService.unsubscribe(DEFAULT_TX_GROUP, mockListener); + } + + +} --------------------------------------------------------------------- To unsubscribe, e-mail: notifications-unsubscr...@seata.apache.org For additional commands, e-mail: notifications-h...@seata.apache.org