From 38ff077b13d856ec0d0b666dfdc459e86cda45ce Mon Sep 17 00:00:00 2001 From: yag Date: Sun, 14 May 2023 16:42:20 +0800 Subject: [PATCH] Support non-junit-binding testing server and junit5 extension. (#25) * Support non-junit-binding testing server and junit5 extension. * Update CHANGES.md * A simple workaround of Config object reuse and cache pollution issue. * Move junit5 extension back to original mock server module. * Move junit5 extension back to original mock server module. --- CHANGES.md | 1 + apollo-mockserver/pom.xml | 15 ++ .../mockserver/ApolloTestingServer.java | 209 ++++++++++++++++++ .../apollo/mockserver/EmbeddedApollo.java | 149 +------------ .../mockserver/MockApolloExtension.java | 54 +++++ .../mockserver/ExtensionLifecycleTest.java | 63 ++++++ 6 files changed, 348 insertions(+), 143 deletions(-) create mode 100644 apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java create mode 100644 apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/MockApolloExtension.java create mode 100644 apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/ExtensionLifecycleTest.java diff --git a/CHANGES.md b/CHANGES.md index 5df63b99..dbca21d5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,5 +6,6 @@ Apollo Java 2.2.0 ------------------ [refactor(apollo-client): Optimize the exception message when failing to retrieve configuration information.](https://github.com/apolloconfig/apollo-java/pull/22) +[Add JUnit5 extension support for apollo mock server.](https://github.com/apolloconfig/apollo-java/pull/25) ------------------ All issues and pull requests are [here](https://github.com/apolloconfig/apollo-java/milestone/2?closed=1) \ No newline at end of file diff --git a/apollo-mockserver/pom.xml b/apollo-mockserver/pom.xml index 978df880..9dec2d50 100644 --- a/apollo-mockserver/pom.xml +++ b/apollo-mockserver/pom.xml @@ -61,6 +61,21 @@ com.squareup.okhttp3 okhttp + + org.junit.jupiter + junit-jupiter-api + true + + + org.junit.jupiter + junit-jupiter + true + + + org.junit.jupiter + junit-jupiter-engine + true + org.springframework.boot spring-boot-starter diff --git a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java new file mode 100644 index 00000000..ada57a1b --- /dev/null +++ b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java @@ -0,0 +1,209 @@ +/* + * Copyright 2023 Apollo Authors + * + * Licensed 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 com.ctrip.framework.apollo.mockserver; + +import com.ctrip.framework.apollo.build.ApolloInjector; +import com.ctrip.framework.apollo.core.ApolloClientSystemConsts; +import com.ctrip.framework.apollo.core.dto.ApolloConfig; +import com.ctrip.framework.apollo.core.dto.ApolloConfigNotification; +import com.ctrip.framework.apollo.core.utils.ResourceUtils; +import com.ctrip.framework.apollo.internals.ConfigServiceLocator; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +public class ApolloTestingServer implements AutoCloseable { + + private static final Logger logger = LoggerFactory.getLogger(EmbeddedApollo.class); + private static final Type notificationType = new TypeToken>() { + }.getType(); + + private static Method CONFIG_SERVICE_LOCATOR_CLEAR; + private static ConfigServiceLocator CONFIG_SERVICE_LOCATOR; + + private static final Gson GSON = new Gson(); + private final Map> addedOrModifiedPropertiesOfNamespace = Maps.newConcurrentMap(); + private final Map> deletedKeysOfNamespace = Maps.newConcurrentMap(); + + private MockWebServer server; + + private boolean started; + + private boolean closed; + + static { + try { + System.setProperty("apollo.longPollingInitialDelayInMills", "0"); + CONFIG_SERVICE_LOCATOR = ApolloInjector.getInstance(ConfigServiceLocator.class); + CONFIG_SERVICE_LOCATOR_CLEAR = ConfigServiceLocator.class.getDeclaredMethod("initConfigServices"); + CONFIG_SERVICE_LOCATOR_CLEAR.setAccessible(true); + } catch (NoSuchMethodException e) { + logger.error(e.getMessage(), e); + } + } + + public void start() throws IOException { + clear(); + server = new MockWebServer(); + final Dispatcher dispatcher = new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) throws InterruptedException { + if (request.getPath().startsWith("/notifications/v2")) { + String notifications = request.getRequestUrl().queryParameter("notifications"); + return new MockResponse().setResponseCode(200).setBody(mockLongPollBody(notifications)); + } + if (request.getPath().startsWith("/configs")) { + List pathSegments = request.getRequestUrl().pathSegments(); + // appId and cluster might be used in the future + String appId = pathSegments.get(1); + String cluster = pathSegments.get(2); + String namespace = pathSegments.get(3); + return new MockResponse().setResponseCode(200).setBody(loadConfigFor(namespace)); + } + return new MockResponse().setResponseCode(404); + } + }; + + server.setDispatcher(dispatcher); + server.start(); + + mockConfigServiceUrl("http://localhost:" + server.getPort()); + started = true; + } + + public void close() { + try { + clear(); + server.close(); + } catch (Exception e) { + logger.error("stop apollo server error", e); + } finally { + closed = true; + } + } + + public boolean isClosed() { + return closed; + } + + public boolean isStarted() { + return started; + } + + private void clear() { + resetOverriddenProperties(); + } + + private void mockConfigServiceUrl(String url) { + System.setProperty(ApolloClientSystemConsts.APOLLO_CONFIG_SERVICE, url); + + try { + CONFIG_SERVICE_LOCATOR_CLEAR.invoke(CONFIG_SERVICE_LOCATOR); + } catch (Exception e) { + throw new IllegalStateException("Invoke config service locator clear failed.", e); + } + } + + private String loadConfigFor(String namespace) { + String filename = String.format("mockdata-%s.properties", namespace); + final Properties prop = ResourceUtils.readConfigFile(filename, new Properties()); + Map configurations = Maps.newHashMap(); + for (String propertyName : prop.stringPropertyNames()) { + configurations.put(propertyName, prop.getProperty(propertyName)); + } + ApolloConfig apolloConfig = new ApolloConfig("someAppId", "someCluster", namespace, "someReleaseKey"); + + Map mergedConfigurations = mergeOverriddenProperties(namespace, configurations); + apolloConfig.setConfigurations(mergedConfigurations); + return GSON.toJson(apolloConfig); + } + + private String mockLongPollBody(String notificationsStr) { + List oldNotifications = GSON.fromJson(notificationsStr, notificationType); + List newNotifications = new ArrayList<>(); + for (ApolloConfigNotification notification : oldNotifications) { + newNotifications + .add(new ApolloConfigNotification(notification.getNamespaceName(), notification.getNotificationId() + 1)); + } + return GSON.toJson(newNotifications); + } + + /** + * 合并用户对namespace的修改 + */ + private Map mergeOverriddenProperties(String namespace, Map configurations) { + if (addedOrModifiedPropertiesOfNamespace.containsKey(namespace)) { + configurations.putAll(addedOrModifiedPropertiesOfNamespace.get(namespace)); + } + if (deletedKeysOfNamespace.containsKey(namespace)) { + for (String k : deletedKeysOfNamespace.get(namespace)) { + configurations.remove(k); + } + } + return configurations; + } + + /** + * Add new property or update existed property + */ + public void addOrModifyProperty(String namespace, String someKey, String someValue) { + if (addedOrModifiedPropertiesOfNamespace.containsKey(namespace)) { + addedOrModifiedPropertiesOfNamespace.get(namespace).put(someKey, someValue); + } else { + Map m = Maps.newConcurrentMap(); + m.put(someKey, someValue); + addedOrModifiedPropertiesOfNamespace.put(namespace, m); + } + } + + /** + * Delete existed property + */ + public void deleteProperty(String namespace, String someKey) { + if (deletedKeysOfNamespace.containsKey(namespace)) { + deletedKeysOfNamespace.get(namespace).add(someKey); + } else { + Set m = Sets.newConcurrentHashSet(); + m.add(someKey); + deletedKeysOfNamespace.put(namespace, m); + } + } + + /** + * reset overridden properties + */ + public void resetOverriddenProperties() { + addedOrModifiedPropertiesOfNamespace.clear(); + deletedKeysOfNamespace.clear(); + } +} diff --git a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/EmbeddedApollo.java b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/EmbeddedApollo.java index 865ad4c2..1f045843 100644 --- a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/EmbeddedApollo.java +++ b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/EmbeddedApollo.java @@ -16,181 +16,44 @@ */ package com.ctrip.framework.apollo.mockserver; -import com.ctrip.framework.apollo.build.ApolloInjector; -import com.ctrip.framework.apollo.core.ApolloClientSystemConsts; -import com.ctrip.framework.apollo.core.dto.ApolloConfig; -import com.ctrip.framework.apollo.core.dto.ApolloConfigNotification; -import com.ctrip.framework.apollo.core.utils.ResourceUtils; -import com.ctrip.framework.apollo.internals.ConfigServiceLocator; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; -import java.lang.reflect.Method; -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.Set; -import okhttp3.mockwebserver.Dispatcher; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import okhttp3.mockwebserver.RecordedRequest; import org.junit.rules.ExternalResource; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Create by zhangzheng on 8/22/18 Email:zhangzheng@youzan.com */ public class EmbeddedApollo extends ExternalResource { - private static final Logger logger = LoggerFactory.getLogger(EmbeddedApollo.class); - private static final Type notificationType = new TypeToken>() { - }.getType(); - - private static Method CONFIG_SERVICE_LOCATOR_CLEAR; - private static ConfigServiceLocator CONFIG_SERVICE_LOCATOR; - - private static final Gson GSON = new Gson(); - private final Map> addedOrModifiedPropertiesOfNamespace = Maps.newConcurrentMap(); - private final Map> deletedKeysOfNamespace = Maps.newConcurrentMap(); - - private MockWebServer server; - - static { - try { - System.setProperty("apollo.longPollingInitialDelayInMills", "0"); - CONFIG_SERVICE_LOCATOR = ApolloInjector.getInstance(ConfigServiceLocator.class); - CONFIG_SERVICE_LOCATOR_CLEAR = ConfigServiceLocator.class.getDeclaredMethod("initConfigServices"); - CONFIG_SERVICE_LOCATOR_CLEAR.setAccessible(true); - } catch (NoSuchMethodException e) { - logger.error(e.getMessage(), e); - } - } + private ApolloTestingServer apollo = new ApolloTestingServer(); @Override protected void before() throws Throwable { - clear(); - server = new MockWebServer(); - final Dispatcher dispatcher = new Dispatcher() { - @Override - public MockResponse dispatch(RecordedRequest request) throws InterruptedException { - if (request.getPath().startsWith("/notifications/v2")) { - String notifications = request.getRequestUrl().queryParameter("notifications"); - return new MockResponse().setResponseCode(200).setBody(mockLongPollBody(notifications)); - } - if (request.getPath().startsWith("/configs")) { - List pathSegments = request.getRequestUrl().pathSegments(); - // appId and cluster might be used in the future - String appId = pathSegments.get(1); - String cluster = pathSegments.get(2); - String namespace = pathSegments.get(3); - return new MockResponse().setResponseCode(200).setBody(loadConfigFor(namespace)); - } - return new MockResponse().setResponseCode(404); - } - }; - - server.setDispatcher(dispatcher); - server.start(); - - mockConfigServiceUrl("http://localhost:" + server.getPort()); - + apollo.start(); super.before(); } @Override protected void after() { - try { - clear(); - server.close(); - } catch (Exception e) { - logger.error("stop apollo server error", e); - } - } - - private void clear() throws Exception { - resetOverriddenProperties(); - } - - private void mockConfigServiceUrl(String url) throws Exception { - System.setProperty(ApolloClientSystemConsts.APOLLO_CONFIG_SERVICE, url); - - CONFIG_SERVICE_LOCATOR_CLEAR.invoke(CONFIG_SERVICE_LOCATOR); - } - - private String loadConfigFor(String namespace) { - String filename = String.format("mockdata-%s.properties", namespace); - final Properties prop = ResourceUtils.readConfigFile(filename, new Properties()); - Map configurations = Maps.newHashMap(); - for (String propertyName : prop.stringPropertyNames()) { - configurations.put(propertyName, prop.getProperty(propertyName)); - } - ApolloConfig apolloConfig = new ApolloConfig("someAppId", "someCluster", namespace, "someReleaseKey"); - - Map mergedConfigurations = mergeOverriddenProperties(namespace, configurations); - apolloConfig.setConfigurations(mergedConfigurations); - return GSON.toJson(apolloConfig); - } - - private String mockLongPollBody(String notificationsStr) { - List oldNotifications = GSON.fromJson(notificationsStr, notificationType); - List newNotifications = new ArrayList<>(); - for (ApolloConfigNotification notification : oldNotifications) { - newNotifications - .add(new ApolloConfigNotification(notification.getNamespaceName(), notification.getNotificationId() + 1)); - } - return GSON.toJson(newNotifications); - } - - /** - * 合并用户对namespace的修改 - */ - private Map mergeOverriddenProperties(String namespace, Map configurations) { - if (addedOrModifiedPropertiesOfNamespace.containsKey(namespace)) { - configurations.putAll(addedOrModifiedPropertiesOfNamespace.get(namespace)); - } - if (deletedKeysOfNamespace.containsKey(namespace)) { - for (String k : deletedKeysOfNamespace.get(namespace)) { - configurations.remove(k); - } - } - return configurations; + apollo.close(); } /** * Add new property or update existed property */ public void addOrModifyProperty(String namespace, String someKey, String someValue) { - if (addedOrModifiedPropertiesOfNamespace.containsKey(namespace)) { - addedOrModifiedPropertiesOfNamespace.get(namespace).put(someKey, someValue); - } else { - Map m = Maps.newConcurrentMap(); - m.put(someKey, someValue); - addedOrModifiedPropertiesOfNamespace.put(namespace, m); - } + apollo.addOrModifyProperty(namespace, someKey, someValue); } /** * Delete existed property */ public void deleteProperty(String namespace, String someKey) { - if (deletedKeysOfNamespace.containsKey(namespace)) { - deletedKeysOfNamespace.get(namespace).add(someKey); - } else { - Set m = Sets.newConcurrentHashSet(); - m.add(someKey); - deletedKeysOfNamespace.put(namespace, m); - } + apollo.deleteProperty(namespace, someKey); } /** * reset overridden properties */ public void resetOverriddenProperties() { - addedOrModifiedPropertiesOfNamespace.clear(); - deletedKeysOfNamespace.clear(); + apollo.resetOverriddenProperties(); } } diff --git a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/MockApolloExtension.java b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/MockApolloExtension.java new file mode 100644 index 00000000..99d422a0 --- /dev/null +++ b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/MockApolloExtension.java @@ -0,0 +1,54 @@ +/* + * Copyright 2023 Apollo Authors + * + * Licensed 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 com.ctrip.framework.apollo.mockserver; + +import org.junit.jupiter.api.extension.AfterTestExecutionCallback; +import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; + +import java.io.IOException; + +public class MockApolloExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback, ParameterResolver { + + private ApolloTestingServer apollo = new ApolloTestingServer(); + + @Override + public void afterTestExecution(ExtensionContext context) { + apollo.close(); + } + + @Override + public void beforeTestExecution(ExtensionContext context) throws IOException { + apollo.start(); + } + + @Override + public boolean supportsParameter( + ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + return parameterContext.getParameter().getType() == ApolloTestingServer.class + && extensionContext.getTestMethod().isPresent(); + } + + @Override + public Object resolveParameter( + ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + return apollo; + } +} diff --git a/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/ExtensionLifecycleTest.java b/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/ExtensionLifecycleTest.java new file mode 100644 index 00000000..e897c5af --- /dev/null +++ b/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/ExtensionLifecycleTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2023 Apollo Authors + * + * Licensed 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 com.ctrip.framework.apollo.mockserver; + +import com.ctrip.framework.apollo.Config; +import com.ctrip.framework.apollo.ConfigService; +import com.ctrip.framework.apollo.core.ConfigConsts; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +@ExtendWith(MockApolloExtension.class) +public class ExtensionLifecycleTest { + + private ApolloTestingServer target; + + @AfterEach + public void after() { + Assertions.assertTrue(target.isClosed()); + } + + @Test + public void testParameterInjection(ApolloTestingServer server) throws Exception { + Assertions.assertTrue(server.isStarted()); + target = server; + Config applicationConfig = ConfigService.getAppConfig(); + + Semaphore latch = new Semaphore(0); + applicationConfig.addChangeListener(event -> latch.release()); + + assertEquals("value1", applicationConfig.getProperty("key1", null)); + assertEquals("value2", applicationConfig.getProperty("key2", null)); + + server.addOrModifyProperty(ConfigConsts.NAMESPACE_APPLICATION, "key2", "newValue2"); + assertTrue(latch.tryAcquire(5, TimeUnit.SECONDS)); + assertEquals("newValue2", applicationConfig.getProperty("key2", null)); + + server.resetOverriddenProperties(); + assertTrue(latch.tryAcquire(5, TimeUnit.SECONDS)); + assertEquals("value2", applicationConfig.getProperty("key2", null)); + } +}