You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by al...@apache.org on 2019/08/05 13:35:39 UTC
[camel] 06/09: CAMEL-13342: Implemented a first version of a
proof-of-concept for camel-testcontainers-spring with JUnit 5
This is an automated email from the ASF dual-hosted git repository.
aldettinger pushed a commit to branch CAMEL-13342-JUNIT5-EXPLORATORY
in repository https://gitbox.apache.org/repos/asf/camel.git
commit d995abb254e892c281494d875cc0dbdab9d26e10
Author: aldettinger <al...@gmail.com>
AuthorDate: Thu Jul 11 10:12:37 2019 +0200
CAMEL-13342: Implemented a first version of a proof-of-concept for camel-testcontainers-spring with JUnit 5
---
.../SpringConsulDefaultServiceCallRouteTest.java | 12 +-
...SpringConsulExpressionServiceCallRouteTest.java | 12 +-
.../SpringConsulRibbonServiceCallRouteTest.java | 12 +-
.../cloud/SpringConsulServiceCallRouteTest.java | 8 +-
.../junit5/spring/CamelAnnotationsHandler.java | 367 ++++++++++++++
.../spring/CamelSpringBootExecutionListener.java | 95 ++++
.../spring/CamelSpringBootJUnit4ClassRunner.java | 33 ++
.../test/junit5/spring/CamelSpringBootRunner.java | 87 ++++
.../CamelSpringDelegatingTestContextLoader.java | 138 ++++++
.../test/junit5/spring/CamelSpringRunner.java | 83 ++++
.../spring/CamelSpringTestContextLoader.java | 551 +++++++++++++++++++++
...ringTestContextLoaderTestExecutionListener.java | 50 ++
.../test/junit5/spring/CamelSpringTestHelper.java | 109 ++++
.../test/junit5/spring/CamelSpringTestSupport.java | 212 ++++++++
.../spring/CamelTestContextBootstrapper.java | 31 ++
.../camel/test/junit5/spring/DisableJmx.java | 43 ++
.../spring/DisableJmxTestExecutionListener.java | 39 ++
.../test/junit5/spring/EnableRouteCoverage.java | 41 ++
.../camel/test/junit5/spring/ExcludeRoutes.java | 44 ++
.../camel/test/junit5/spring/MockEndpoints.java | 43 ++
.../test/junit5/spring/MockEndpointsAndSkip.java | 43 ++
.../test/junit5/spring/ProvidesBreakpoint.java | 36 ++
.../test/junit5/spring/RouteCoverageDumper.java | 82 +++
.../junit5/spring/RouteCoverageEventNotifier.java | 51 ++
.../camel/test/junit5/spring/ShutdownTimeout.java | 49 ++
.../spring/StopWatchTestExecutionListener.java | 62 +++
.../camel/test/junit5/spring/UseAdviceWith.java | 49 ++
...eOverridePropertiesWithPropertiesComponent.java | 34 ++
.../spring/ContainerAwareSpringTestSupport.java | 112 +++++
.../spring/ContainerAwareSpringTestSupportIT.java | 61 +++
30 files changed, 2567 insertions(+), 22 deletions(-)
diff --git a/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulDefaultServiceCallRouteTest.java b/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulDefaultServiceCallRouteTest.java
index e0cbcb9..bbe67f2 100644
--- a/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulDefaultServiceCallRouteTest.java
+++ b/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulDefaultServiceCallRouteTest.java
@@ -20,8 +20,8 @@ import java.util.List;
import org.apache.camel.component.ribbon.cloud.RibbonServiceLoadBalancer;
import org.apache.camel.impl.cloud.DefaultServiceCallProcessor;
-import org.junit.Assert;
-import org.junit.Test;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
@@ -35,9 +35,9 @@ public class SpringConsulDefaultServiceCallRouteTest extends SpringConsulService
public void testServiceCallConfiguration() throws Exception {
List<DefaultServiceCallProcessor> processors = findServiceCallProcessors();
- Assert.assertFalse(processors.isEmpty());
- Assert.assertEquals(2, processors.size());
- Assert.assertFalse(processors.get(0).getLoadBalancer() instanceof RibbonServiceLoadBalancer);
- Assert.assertFalse(processors.get(1).getLoadBalancer() instanceof RibbonServiceLoadBalancer);
+ Assertions.assertFalse(processors.isEmpty());
+ Assertions.assertEquals(2, processors.size());
+ Assertions.assertFalse(processors.get(0).getLoadBalancer() instanceof RibbonServiceLoadBalancer);
+ Assertions.assertFalse(processors.get(1).getLoadBalancer() instanceof RibbonServiceLoadBalancer);
}
}
diff --git a/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulExpressionServiceCallRouteTest.java b/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulExpressionServiceCallRouteTest.java
index 21995d7..f1f937f 100644
--- a/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulExpressionServiceCallRouteTest.java
+++ b/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulExpressionServiceCallRouteTest.java
@@ -20,8 +20,8 @@ import java.util.List;
import org.apache.camel.component.ribbon.cloud.RibbonServiceLoadBalancer;
import org.apache.camel.impl.cloud.DefaultServiceCallProcessor;
-import org.junit.Assert;
-import org.junit.Test;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
@@ -35,9 +35,9 @@ public class SpringConsulExpressionServiceCallRouteTest extends SpringConsulServ
public void testServiceCallConfiguration() throws Exception {
List<DefaultServiceCallProcessor> processors = findServiceCallProcessors();
- Assert.assertFalse(processors.isEmpty());
- Assert.assertEquals(2, processors.size());
- Assert.assertFalse(processors.get(0).getLoadBalancer() instanceof RibbonServiceLoadBalancer);
- Assert.assertFalse(processors.get(1).getLoadBalancer() instanceof RibbonServiceLoadBalancer);
+ Assertions.assertFalse(processors.isEmpty());
+ Assertions.assertEquals(2, processors.size());
+ Assertions.assertFalse(processors.get(0).getLoadBalancer() instanceof RibbonServiceLoadBalancer);
+ Assertions.assertFalse(processors.get(1).getLoadBalancer() instanceof RibbonServiceLoadBalancer);
}
}
diff --git a/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulRibbonServiceCallRouteTest.java b/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulRibbonServiceCallRouteTest.java
index 230d178..b8cd79b 100644
--- a/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulRibbonServiceCallRouteTest.java
+++ b/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulRibbonServiceCallRouteTest.java
@@ -20,8 +20,8 @@ import java.util.List;
import org.apache.camel.component.ribbon.cloud.RibbonServiceLoadBalancer;
import org.apache.camel.impl.cloud.DefaultServiceCallProcessor;
-import org.junit.Assert;
-import org.junit.Test;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
@@ -35,9 +35,9 @@ public class SpringConsulRibbonServiceCallRouteTest extends SpringConsulServiceC
public void testServiceCallConfiguration() throws Exception {
List<DefaultServiceCallProcessor> processors = findServiceCallProcessors();
- Assert.assertFalse(processors.isEmpty());
- Assert.assertEquals(2, processors.size());
- Assert.assertTrue(processors.get(0).getLoadBalancer() instanceof RibbonServiceLoadBalancer);
- Assert.assertTrue(processors.get(1).getLoadBalancer() instanceof RibbonServiceLoadBalancer);
+ Assertions.assertFalse(processors.isEmpty());
+ Assertions.assertEquals(2, processors.size());
+ Assertions.assertTrue(processors.get(0).getLoadBalancer() instanceof RibbonServiceLoadBalancer);
+ Assertions.assertTrue(processors.get(1).getLoadBalancer() instanceof RibbonServiceLoadBalancer);
}
}
diff --git a/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulServiceCallRouteTest.java b/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulServiceCallRouteTest.java
index 68de60c..83a4608 100644
--- a/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulServiceCallRouteTest.java
+++ b/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulServiceCallRouteTest.java
@@ -31,9 +31,9 @@ import org.apache.camel.component.consul.ConsulTestSupport;
import org.apache.camel.impl.cloud.DefaultServiceCallProcessor;
import org.apache.camel.processor.ChoiceProcessor;
import org.apache.camel.processor.FilterProcessor;
-import org.apache.camel.test.testcontainers.spring.ContainerAwareSpringTestSupport;
-import org.junit.Assert;
-import org.junit.Test;
+import org.apache.camel.test.junit5.testcontainers.spring.ContainerAwareSpringTestSupport;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
public abstract class SpringConsulServiceCallRouteTest extends ContainerAwareSpringTestSupport {
@@ -130,7 +130,7 @@ public abstract class SpringConsulServiceCallRouteTest extends ContainerAwareSpr
protected List<DefaultServiceCallProcessor> findServiceCallProcessors() {
Route route = context().getRoute("scall");
- Assert.assertNotNull("ServiceCall Route should be present", route);
+ Assertions.assertNotNull(route, "ServiceCall Route should be present");
return findServiceCallProcessors(new ArrayList<>(), route.navigate());
}
diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelAnnotationsHandler.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelAnnotationsHandler.java
new file mode 100644
index 0000000..8bc45b2
--- /dev/null
+++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelAnnotationsHandler.java
@@ -0,0 +1,367 @@
+/*
+ * 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.camel.test.junit5.spring;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Properties;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+
+import org.apache.camel.ExtendedCamelContext;
+import org.apache.camel.api.management.JmxSystemPropertyKeys;
+import org.apache.camel.api.management.ManagedCamelContext;
+import org.apache.camel.api.management.mbean.ManagedCamelContextMBean;
+import org.apache.camel.component.properties.PropertiesComponent;
+import org.apache.camel.impl.engine.InterceptSendToMockEndpointStrategy;
+import org.apache.camel.processor.interceptor.DefaultDebugger;
+import org.apache.camel.spi.Breakpoint;
+import org.apache.camel.spi.Debugger;
+import org.apache.camel.spi.EventNotifier;
+import org.apache.camel.spring.SpringCamelContext;
+import org.apache.camel.test.junit5.CamelTestSupport;
+import org.apache.camel.util.CollectionStringBuffer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.annotation.AnnotationUtils;
+
+import static org.apache.camel.test.spring.CamelSpringTestHelper.getAllMethods;
+
+public final class CamelAnnotationsHandler {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(CamelAnnotationsHandler.class);
+
+ private CamelAnnotationsHandler() {
+ }
+
+ /**
+ * Handles @ExcludeRoutes to make it easier to exclude other routes when testing with Spring Boot.
+ *
+ * @param testClass the test class being executed
+ */
+ public static void handleExcludeRoutesForSpringBoot(Class<?> testClass) {
+ if (testClass.isAnnotationPresent(ExcludeRoutes.class)) {
+ Class[] routes = testClass.getAnnotation(ExcludeRoutes.class).value();
+ // need to setup this as a JVM system property
+ CollectionStringBuffer csb = new CollectionStringBuffer(",");
+ for (Class clazz : routes) {
+ csb.append(clazz.getName());
+ }
+ String key = "CamelTestSpringExcludeRoutes";
+ String value = csb.toString();
+
+ String exists = System.getProperty(key);
+ if (exists != null) {
+ LOGGER.warn("Cannot use @ExcludeRoutes as JVM property " + key + " has already been set.");
+ } else {
+ LOGGER.info("@ExcludeRoutes annotation found. Setting up JVM property {}={}", key, value);
+ System.setProperty(key, value);
+ }
+ }
+ }
+
+ /**
+ * Handles disabling of JMX on Camel contexts based on {@link DisableJmx}.
+ *
+ * @param context the initialized Spring context
+ * @param testClass the test class being executed
+ */
+ public static void handleDisableJmx(ConfigurableApplicationContext context, Class<?> testClass) {
+ CamelSpringTestHelper.setOriginalJmxDisabledValue(System.getProperty(JmxSystemPropertyKeys.DISABLED));
+
+ if (testClass.isAnnotationPresent(DisableJmx.class)) {
+ if (testClass.getAnnotation(DisableJmx.class).value()) {
+ LOGGER.info("Disabling Camel JMX globally as DisableJmx annotation was found and disableJmx is set to true.");
+ System.setProperty(JmxSystemPropertyKeys.DISABLED, "true");
+
+ } else {
+ LOGGER.info("Enabling Camel JMX as DisableJmx annotation was found and disableJmx is set to false.");
+ System.clearProperty(JmxSystemPropertyKeys.DISABLED);
+ }
+ } else {
+ LOGGER.info("Disabling Camel JMX globally for tests by default. Use the DisableJMX annotation to override the default setting.");
+ System.setProperty(JmxSystemPropertyKeys.DISABLED, "true");
+ }
+ }
+
+ /**
+ * Handles disabling of JMX on Camel contexts based on {@link DisableJmx}.
+ *
+ * @param context the initialized Spring context
+ * @param testClass the test class being executed
+ */
+ public static void handleRouteCoverage(ConfigurableApplicationContext context, Class<?> testClass, Function testMethod) throws Exception {
+ if (testClass.isAnnotationPresent(EnableRouteCoverage.class)) {
+ System.setProperty(CamelTestSupport.ROUTE_COVERAGE_ENABLED, "true");
+
+ CamelSpringTestHelper.doToSpringCamelContexts(context, new CamelSpringTestHelper.DoToSpringCamelContextsStrategy() {
+
+ @Override
+ public void execute(String contextName, SpringCamelContext camelContext) throws Exception {
+ LOGGER.info("Enabling RouteCoverage");
+ EventNotifier notifier = new RouteCoverageEventNotifier(testClass.getName(), testMethod);
+ camelContext.addService(notifier, true);
+ camelContext.getManagementStrategy().addEventNotifier(notifier);
+ }
+ });
+ }
+ }
+
+ public static void handleRouteCoverageDump(ConfigurableApplicationContext context, Class<?> testClass, Function testMethod) throws Exception {
+ if (testClass.isAnnotationPresent(EnableRouteCoverage.class)) {
+ CamelSpringTestHelper.doToSpringCamelContexts(context, new CamelSpringTestHelper.DoToSpringCamelContextsStrategy() {
+
+ @Override
+ public void execute(String contextName, SpringCamelContext camelContext) throws Exception {
+ LOGGER.debug("Dumping RouteCoverage");
+
+ String testMethodName = (String) testMethod.apply(this);
+ RouteCoverageDumper.dumpRouteCoverage(camelContext, testClass.getName(), testMethodName);
+
+ // reset JMX statistics
+ ManagedCamelContextMBean managedCamelContext = camelContext.getExtension(ManagedCamelContext.class).getManagedCamelContext();
+ if (managedCamelContext != null) {
+ LOGGER.debug("Resetting JMX statistics for RouteCoverage");
+ managedCamelContext.reset(true);
+ }
+
+ // turn off dumping one more time by removing the event listener (which would dump as well when Camel is stopping)
+ // but this method was explicit invoked to dump such as from afterTest callbacks from JUnit.
+ RouteCoverageEventNotifier eventNotifier = camelContext.hasService(RouteCoverageEventNotifier.class);
+ if (eventNotifier != null) {
+ camelContext.getManagementStrategy().removeEventNotifier(eventNotifier);
+ camelContext.removeService(eventNotifier);
+ }
+ }
+ });
+ }
+ }
+
+ public static void handleProvidesBreakpoint(ConfigurableApplicationContext context, Class<?> testClass) throws Exception {
+ Collection<Method> methods = getAllMethods(testClass);
+ final List<Breakpoint> breakpoints = new LinkedList<>();
+
+ for (Method method : methods) {
+ if (AnnotationUtils.findAnnotation(method, ProvidesBreakpoint.class) != null) {
+ Class<?>[] argTypes = method.getParameterTypes();
+ if (argTypes.length != 0) {
+ throw new IllegalArgumentException("Method [" + method.getName()
+ + "] is annotated with ProvidesBreakpoint but is not a no-argument method.");
+ } else if (!Breakpoint.class.isAssignableFrom(method.getReturnType())) {
+ throw new IllegalArgumentException("Method [" + method.getName()
+ + "] is annotated with ProvidesBreakpoint but does not return a Breakpoint.");
+ } else if (!Modifier.isStatic(method.getModifiers())) {
+ throw new IllegalArgumentException("Method [" + method.getName()
+ + "] is annotated with ProvidesBreakpoint but is not static.");
+ } else if (!Modifier.isPublic(method.getModifiers())) {
+ throw new IllegalArgumentException("Method [" + method.getName()
+ + "] is annotated with ProvidesBreakpoint but is not public.");
+ }
+
+ try {
+ breakpoints.add((Breakpoint) method.invoke(null));
+ } catch (Exception e) {
+ throw new RuntimeException("Method [" + method.getName()
+ + "] threw exception during evaluation.", e);
+ }
+ }
+ }
+
+ if (breakpoints.size() != 0) {
+ CamelSpringTestHelper.doToSpringCamelContexts(context, new CamelSpringTestHelper.DoToSpringCamelContextsStrategy() {
+
+ public void execute(String contextName, SpringCamelContext camelContext)
+ throws Exception {
+ Debugger debugger = camelContext.getDebugger();
+ if (debugger == null) {
+ debugger = new DefaultDebugger();
+ camelContext.setDebugger(debugger);
+ }
+
+ for (Breakpoint breakpoint : breakpoints) {
+ LOGGER.info("Adding Breakpoint [{}] to CamelContext with name [{}].", breakpoint, contextName);
+ debugger.addBreakpoint(breakpoint);
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * Handles updating shutdown timeouts on Camel contexts based on {@link ShutdownTimeout}.
+ *
+ * @param context the initialized Spring context
+ * @param testClass the test class being executed
+ */
+ public static void handleShutdownTimeout(ConfigurableApplicationContext context, Class<?> testClass) throws Exception {
+ final int shutdownTimeout;
+ final TimeUnit shutdownTimeUnit;
+ if (testClass.isAnnotationPresent(ShutdownTimeout.class)) {
+ shutdownTimeout = testClass.getAnnotation(ShutdownTimeout.class).value();
+ shutdownTimeUnit = testClass.getAnnotation(ShutdownTimeout.class).timeUnit();
+ } else {
+ shutdownTimeout = 10;
+ shutdownTimeUnit = TimeUnit.SECONDS;
+ }
+
+ CamelSpringTestHelper.doToSpringCamelContexts(context, new CamelSpringTestHelper.DoToSpringCamelContextsStrategy() {
+
+ public void execute(String contextName, SpringCamelContext camelContext)
+ throws Exception {
+ LOGGER.info("Setting shutdown timeout to [{} {}] on CamelContext with name [{}].", shutdownTimeout, shutdownTimeUnit, contextName);
+ camelContext.getShutdownStrategy().setTimeout(shutdownTimeout);
+ camelContext.getShutdownStrategy().setTimeUnit(shutdownTimeUnit);
+ }
+ });
+ }
+
+ /**
+ * Handles auto-intercepting of endpoints with mocks based on {@link MockEndpoints}.
+ *
+ * @param context the initialized Spring context
+ * @param testClass the test class being executed
+ */
+ public static void handleMockEndpoints(ConfigurableApplicationContext context, Class<?> testClass) throws Exception {
+ if (testClass.isAnnotationPresent(MockEndpoints.class)) {
+ final String mockEndpoints = testClass.getAnnotation(MockEndpoints.class).value();
+ CamelSpringTestHelper.doToSpringCamelContexts(context, new CamelSpringTestHelper.DoToSpringCamelContextsStrategy() {
+
+ public void execute(String contextName, SpringCamelContext camelContext)
+ throws Exception {
+ LOGGER.info("Enabling auto mocking of endpoints matching pattern [{}] on CamelContext with name [{}].", mockEndpoints, contextName);
+ camelContext.adapt(ExtendedCamelContext.class).registerEndpointCallback(new InterceptSendToMockEndpointStrategy(mockEndpoints));
+ }
+ });
+ }
+ }
+
+ /**
+ * Handles auto-intercepting of endpoints with mocks based on {@link MockEndpointsAndSkip} and skipping the
+ * original endpoint.
+ *
+ * @param context the initialized Spring context
+ * @param testClass the test class being executed
+ */
+ public static void handleMockEndpointsAndSkip(ConfigurableApplicationContext context, Class<?> testClass) throws Exception {
+ if (testClass.isAnnotationPresent(MockEndpointsAndSkip.class)) {
+ final String mockEndpoints = testClass.getAnnotation(MockEndpointsAndSkip.class).value();
+ CamelSpringTestHelper.doToSpringCamelContexts(context, new CamelSpringTestHelper.DoToSpringCamelContextsStrategy() {
+
+ public void execute(String contextName, SpringCamelContext camelContext)
+ throws Exception {
+ // resolve the property place holders of the mockEndpoints
+ String mockEndpointsValue = camelContext.resolvePropertyPlaceholders(mockEndpoints);
+ LOGGER.info("Enabling auto mocking and skipping of endpoints matching pattern [{}] on CamelContext with name [{}].", mockEndpointsValue, contextName);
+ camelContext.adapt(ExtendedCamelContext.class).registerEndpointCallback(new InterceptSendToMockEndpointStrategy(mockEndpointsValue, true));
+ }
+ });
+ }
+ }
+
+ /**
+ * Handles override this method to include and override properties with the Camel {@link org.apache.camel.component.properties.PropertiesComponent}.
+ *
+ * @param context the initialized Spring context
+ * @param testClass the test class being executed
+ */
+ public static void handleUseOverridePropertiesWithPropertiesComponent(ConfigurableApplicationContext context, Class<?> testClass) throws Exception {
+ Collection<Method> methods = getAllMethods(testClass);
+ final List<Properties> properties = new LinkedList<>();
+
+ for (Method method : methods) {
+ if (AnnotationUtils.findAnnotation(method, UseOverridePropertiesWithPropertiesComponent.class) != null) {
+ Class<?>[] argTypes = method.getParameterTypes();
+ if (argTypes.length > 0) {
+ throw new IllegalArgumentException("Method [" + method.getName()
+ + "] is annotated with UseOverridePropertiesWithPropertiesComponent but is not a no-argument method.");
+ } else if (!Properties.class.isAssignableFrom(method.getReturnType())) {
+ throw new IllegalArgumentException("Method [" + method.getName()
+ + "] is annotated with UseOverridePropertiesWithPropertiesComponent but does not return a java.util.Properties.");
+ } else if (!Modifier.isStatic(method.getModifiers())) {
+ throw new IllegalArgumentException("Method [" + method.getName()
+ + "] is annotated with UseOverridePropertiesWithPropertiesComponent but is not static.");
+ } else if (!Modifier.isPublic(method.getModifiers())) {
+ throw new IllegalArgumentException("Method [" + method.getName()
+ + "] is annotated with UseOverridePropertiesWithPropertiesComponent but is not public.");
+ }
+
+ try {
+ properties.add((Properties) method.invoke(null));
+ } catch (Exception e) {
+ throw new RuntimeException("Method [" + method.getName()
+ + "] threw exception during evaluation.", e);
+ }
+ }
+ }
+
+ if (properties.size() != 0) {
+ CamelSpringTestHelper.doToSpringCamelContexts(context, new CamelSpringTestHelper.DoToSpringCamelContextsStrategy() {
+ public void execute(String contextName, SpringCamelContext camelContext) throws Exception {
+ PropertiesComponent pc = camelContext.getComponent("properties", PropertiesComponent.class);
+ Properties extra = new Properties();
+ for (Properties prop : properties) {
+ extra.putAll(prop);
+ }
+ if (!extra.isEmpty()) {
+ LOGGER.info("Using {} properties to override any existing properties on the PropertiesComponent on CamelContext with name [{}].", extra.size(), contextName);
+ pc.setOverrideProperties(extra);
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * Handles starting of Camel contexts based on {@link UseAdviceWith} and other state in the JVM.
+ *
+ * @param context the initialized Spring context
+ * @param testClass the test class being executed
+ */
+ public static void handleCamelContextStartup(ConfigurableApplicationContext context, Class<?> testClass) throws Exception {
+ boolean skip = "true".equalsIgnoreCase(System.getProperty("skipStartingCamelContext"));
+ if (skip) {
+ LOGGER.info("Skipping starting CamelContext(s) as system property skipStartingCamelContext is set to be true.");
+ } else if (testClass.isAnnotationPresent(UseAdviceWith.class)) {
+ if (testClass.getAnnotation(UseAdviceWith.class).value()) {
+ LOGGER.info("Skipping starting CamelContext(s) as UseAdviceWith annotation was found and isUseAdviceWith is set to true.");
+ skip = true;
+ } else {
+ LOGGER.info("Starting CamelContext(s) as UseAdviceWith annotation was found, but isUseAdviceWith is set to false.");
+ skip = false;
+ }
+ }
+
+ if (!skip) {
+ CamelSpringTestHelper.doToSpringCamelContexts(context, new CamelSpringTestHelper.DoToSpringCamelContextsStrategy() {
+ public void execute(String contextName,
+ SpringCamelContext camelContext) throws Exception {
+ if (!camelContext.isStarted()) {
+ LOGGER.info("Starting CamelContext with name [{}].", contextName);
+ camelContext.start();
+ } else {
+ LOGGER.debug("CamelContext with name [{}] already started.", contextName);
+ }
+ }
+ });
+ }
+ }
+
+}
diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringBootExecutionListener.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringBootExecutionListener.java
new file mode 100644
index 0000000..4192e5e
--- /dev/null
+++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringBootExecutionListener.java
@@ -0,0 +1,95 @@
+/*
+ * 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.camel.test.junit5.spring;
+
+import org.apache.camel.spring.SpringCamelContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.test.context.TestContext;
+import org.springframework.test.context.support.AbstractTestExecutionListener;
+
+public class CamelSpringBootExecutionListener extends AbstractTestExecutionListener {
+
+ protected static ThreadLocal<ConfigurableApplicationContext> threadApplicationContext = new ThreadLocal<>();
+
+ private static final Logger LOG = LoggerFactory.getLogger(CamelSpringBootExecutionListener.class);
+
+ @Override
+ public void prepareTestInstance(TestContext testContext) throws Exception {
+ LOG.info("@RunWith(CamelSpringBootRunner.class) preparing: {}", testContext.getTestClass());
+
+ Class<?> testClass = testContext.getTestClass();
+
+ // need to prepare this before we load spring application context
+ CamelAnnotationsHandler.handleExcludeRoutesForSpringBoot(testClass);
+
+ // we are customizing the Camel context with
+ // CamelAnnotationsHandler so we do not want to start it
+ // automatically, which would happen when SpringCamelContext
+ // is added to Spring ApplicationContext, so we set the flag
+ // not to start it just yet
+ SpringCamelContext.setNoStart(true);
+ System.setProperty("skipStartingCamelContext", "true");
+ ConfigurableApplicationContext context = (ConfigurableApplicationContext) testContext.getApplicationContext();
+
+ // Post CamelContext(s) instantiation but pre CamelContext(s) start setup
+ CamelAnnotationsHandler.handleProvidesBreakpoint(context, testClass);
+ CamelAnnotationsHandler.handleShutdownTimeout(context, testClass);
+ CamelAnnotationsHandler.handleMockEndpoints(context, testClass);
+ CamelAnnotationsHandler.handleMockEndpointsAndSkip(context, testClass);
+ CamelAnnotationsHandler.handleUseOverridePropertiesWithPropertiesComponent(context, testClass);
+
+ System.clearProperty("skipStartingCamelContext");
+ SpringCamelContext.setNoStart(false);
+ }
+
+ @Override
+ public void beforeTestMethod(TestContext testContext) throws Exception {
+ LOG.info("@RunWith(CamelSpringBootRunner.class) before: {}.{}", testContext.getTestClass(), testContext.getTestMethod().getName());
+
+ Class<?> testClass = testContext.getTestClass();
+ String testName = testContext.getTestMethod().getName();
+
+ ConfigurableApplicationContext context = (ConfigurableApplicationContext) testContext.getApplicationContext();
+ threadApplicationContext.set(context);
+
+ // mark Camel to be startable again and start Camel
+ System.clearProperty("skipStartingCamelContext");
+
+ // route coverage need to know the test method
+ CamelAnnotationsHandler.handleRouteCoverage(context, testClass, s -> testName);
+
+ LOG.info("Initialized CamelSpringBootRunner now ready to start CamelContext");
+ CamelAnnotationsHandler.handleCamelContextStartup(context, testClass);
+ }
+
+ @Override
+ public void afterTestMethod(TestContext testContext) throws Exception {
+ LOG.info("@RunWith(CamelSpringBootRunner.class) after: {}.{}", testContext.getTestClass(), testContext.getTestMethod().getName());
+
+ Class<?> testClass = testContext.getTestClass();
+ String testName = testContext.getTestMethod().getName();
+
+ ConfigurableApplicationContext context = threadApplicationContext.get();
+ if (context != null && context.isRunning()) {
+ // dump route coverage for each test method so its accurate statistics
+ // even if spring application context is running (i.e. its not dirtied per test method)
+ CamelAnnotationsHandler.handleRouteCoverageDump(context, testClass, s -> testName);
+ }
+ }
+}
diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringBootJUnit4ClassRunner.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringBootJUnit4ClassRunner.java
new file mode 100644
index 0000000..d96c903
--- /dev/null
+++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringBootJUnit4ClassRunner.java
@@ -0,0 +1,33 @@
+/*
+ * 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.camel.test.junit5.spring;
+
+import org.junit.runners.model.InitializationError;
+
+/**
+ * The class {@link CamelSpringBootJUnit4ClassRunner} has been renamed to {@link CamelSpringBootRunner}
+ * which is a shorter and easier to remember name.
+ *
+ * @deprecated use {@link CamelSpringBootRunner}
+ */
+@Deprecated
+public class CamelSpringBootJUnit4ClassRunner extends CamelSpringBootRunner {
+
+ public CamelSpringBootJUnit4ClassRunner(Class<?> clazz) throws InitializationError {
+ super(clazz);
+ }
+}
diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringBootRunner.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringBootRunner.java
new file mode 100644
index 0000000..fbd3a2e
--- /dev/null
+++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringBootRunner.java
@@ -0,0 +1,87 @@
+/*
+ * 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.camel.test.junit5.spring;
+
+import java.util.List;
+
+import org.junit.runners.model.InitializationError;
+import org.springframework.test.context.TestContextManager;
+import org.springframework.test.context.TestExecutionListener;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+/**
+ * An implementation bringing the functionality of {@link CamelSpringTestSupport} to
+ * Spring Boot Test based test cases. This approach allows developers to implement tests
+ * for their Spring Boot based applications/routes using the typical Spring Test conventions
+ * for test development.
+ */
+public class CamelSpringBootRunner extends SpringJUnit4ClassRunner {
+
+ public CamelSpringBootRunner(Class<?> clazz) throws InitializationError {
+ super(clazz);
+ }
+
+ /**
+ * Returns the specialized manager instance that provides tight integration between Camel testing
+ * features and Spring.
+ *
+ * @return a new instance of {@link CamelTestContextManager}.
+ */
+ @Override
+ protected TestContextManager createTestContextManager(Class<?> clazz) {
+ return new CamelTestContextManager(clazz);
+ }
+
+ /**
+ * An implementation providing additional integration between Spring Test and Camel
+ * testing features.
+ */
+ public static final class CamelTestContextManager extends TestContextManager {
+
+ public CamelTestContextManager(Class<?> testClass) {
+ super(testClass);
+
+ // turn off auto starting spring as we need to do this later
+ System.setProperty("skipStartingCamelContext", "true");
+
+ // is Camel already registered
+ if (!alreadyRegistered()) {
+ // inject Camel first, and then disable jmx and add the stop-watch
+ List<TestExecutionListener> list = getTestExecutionListeners();
+ list.add(0, new CamelSpringTestContextLoaderTestExecutionListener());
+ list.add(1, new DisableJmxTestExecutionListener());
+ list.add(2, new CamelSpringBootExecutionListener());
+ list.add(3, new StopWatchTestExecutionListener());
+ }
+ }
+
+ private boolean alreadyRegistered() {
+ List<TestExecutionListener> list = getTestExecutionListeners();
+ if (list != null) {
+ for (TestExecutionListener listener : list) {
+ if (listener instanceof CamelSpringTestContextLoaderTestExecutionListener) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ }
+
+}
diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringDelegatingTestContextLoader.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringDelegatingTestContextLoader.java
new file mode 100644
index 0000000..ce486e5
--- /dev/null
+++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringDelegatingTestContextLoader.java
@@ -0,0 +1,138 @@
+/*
+ * 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.camel.test.junit5.spring;
+
+import java.lang.reflect.Method;
+
+import org.apache.camel.api.management.JmxSystemPropertyKeys;
+import org.apache.camel.spring.SpringCamelContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.AnnotationConfigUtils;
+import org.springframework.test.context.MergedContextConfiguration;
+import org.springframework.test.context.support.DelegatingSmartContextLoader;
+
+/**
+ * CamelSpringDelegatingTestContextLoader which fixes issues in Camel's JavaConfigContextLoader. (adds support for Camel's test annotations)
+ * <br>
+ * <em>This loader can handle either classes or locations for configuring the context.</em>
+ * <br>
+ * NOTE: This TestContextLoader doesn't support the annotation of ExcludeRoutes now.
+ *
+ * @deprecated use {@link CamelSpringRunner} or {@link CamelSpringBootRunner} instead.
+ */
+@Deprecated
+public class CamelSpringDelegatingTestContextLoader extends DelegatingSmartContextLoader {
+
+ protected final Logger logger = LoggerFactory.getLogger(getClass());
+
+ @Override
+ public ApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception {
+
+ Class<?> testClass = getTestClass();
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("Loading ApplicationContext for merged context configuration [{}].", mergedConfig);
+ }
+
+ // Pre CamelContext(s) instantiation setup
+ CamelAnnotationsHandler.handleDisableJmx(null, testClass);
+
+ try {
+ SpringCamelContext.setNoStart(true);
+ System.setProperty("skipStartingCamelContext", "true");
+ ConfigurableApplicationContext context = (ConfigurableApplicationContext) super.loadContext(mergedConfig);
+ SpringCamelContext.setNoStart(false);
+ System.clearProperty("skipStartingCamelContext");
+ return loadContext(context, testClass);
+ } finally {
+ cleanup(testClass);
+ }
+ }
+
+ /**
+ * Performs the bulk of the Spring application context loading/customization.
+ *
+ * @param context the partially configured context. The context should have the bean definitions loaded, but nothing else.
+ * @param testClass the test class being executed
+ * @return the initialized (refreshed) Spring application context
+ *
+ * @throws Exception if there is an error during initialization/customization
+ */
+ public ApplicationContext loadContext(ConfigurableApplicationContext context, Class<?> testClass)
+ throws Exception {
+
+ AnnotationConfigUtils.registerAnnotationConfigProcessors((BeanDefinitionRegistry) context);
+
+ // Post CamelContext(s) instantiation but pre CamelContext(s) start setup
+ CamelAnnotationsHandler.handleRouteCoverage(context, testClass, s -> getTestMethod().getName());
+ CamelAnnotationsHandler.handleProvidesBreakpoint(context, testClass);
+ CamelAnnotationsHandler.handleShutdownTimeout(context, testClass);
+ CamelAnnotationsHandler.handleMockEndpoints(context, testClass);
+ CamelAnnotationsHandler.handleMockEndpointsAndSkip(context, testClass);
+ CamelAnnotationsHandler.handleUseOverridePropertiesWithPropertiesComponent(context, testClass);
+
+ // CamelContext(s) startup
+ CamelAnnotationsHandler.handleCamelContextStartup(context, testClass);
+
+ return context;
+ }
+
+ /**
+ * Cleanup/restore global state to defaults / pre-test values after the test setup
+ * is complete.
+ *
+ * @param testClass the test class being executed
+ */
+ protected void cleanup(Class<?> testClass) {
+ SpringCamelContext.setNoStart(false);
+
+ if (testClass.isAnnotationPresent(DisableJmx.class)) {
+ if (CamelSpringTestHelper.getOriginalJmxDisabled() == null) {
+ System.clearProperty(JmxSystemPropertyKeys.DISABLED);
+ } else {
+ System.setProperty(JmxSystemPropertyKeys.DISABLED,
+ CamelSpringTestHelper.getOriginalJmxDisabled());
+ }
+ }
+ }
+
+ /**
+ * Returns the class under test in order to enable inspection of annotations while the
+ * Spring context is being created.
+ *
+ * @return the test class that is being executed
+ * @see CamelSpringTestHelper
+ */
+ protected Class<?> getTestClass() {
+ return CamelSpringTestHelper.getTestClass();
+ }
+
+ /**
+ * Returns the test method under test.
+ *
+ * @return the method that is being executed
+ * @see CamelSpringTestHelper
+ */
+ protected Method getTestMethod() {
+ return CamelSpringTestHelper.getTestMethod();
+ }
+
+}
diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringRunner.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringRunner.java
new file mode 100644
index 0000000..d64ce29
--- /dev/null
+++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringRunner.java
@@ -0,0 +1,83 @@
+/*
+ * 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.camel.test.junit5.spring;
+
+import java.util.List;
+
+import org.junit.runners.model.InitializationError;
+import org.springframework.test.context.TestContextManager;
+import org.springframework.test.context.TestExecutionListener;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+/**
+ * An implementation bringing the functionality of {@link org.apache.camel.test.spring.CamelSpringTestSupport} to
+ * Spring Test based test cases. This approach allows developers to implement tests
+ * for their Spring based applications/routes using the typical Spring Test conventions
+ * for test development.
+ */
+public class CamelSpringRunner extends SpringJUnit4ClassRunner {
+
+ public CamelSpringRunner(Class<?> clazz) throws InitializationError {
+ super(clazz);
+ }
+
+ /**
+ * Returns the specialized manager instance that provides tight integration between Camel testing
+ * features and Spring.
+ *
+ * @return a new instance of {@link CamelTestContextManager}.
+ */
+ @Override
+ protected TestContextManager createTestContextManager(Class<?> clazz) {
+ return new CamelTestContextManager(clazz);
+ }
+
+ /**
+ * An implementation providing additional integration between Spring Test and Camel
+ * testing features.
+ */
+ public static final class CamelTestContextManager extends TestContextManager {
+
+ public CamelTestContextManager(Class<?> testClass) {
+ super(testClass);
+
+ // is Camel already registered
+ if (!alreadyRegistered()) {
+ // inject Camel first, and then disable jmx and add the stop-watch
+ List<TestExecutionListener> list = getTestExecutionListeners();
+ list.add(0, new CamelSpringTestContextLoaderTestExecutionListener());
+ list.add(1, new DisableJmxTestExecutionListener());
+ list.add(2, new StopWatchTestExecutionListener());
+ }
+ }
+
+ private boolean alreadyRegistered() {
+ List<TestExecutionListener> list = getTestExecutionListeners();
+ if (list != null) {
+ for (TestExecutionListener listener : list) {
+ if (listener instanceof CamelSpringTestContextLoaderTestExecutionListener) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ }
+
+}
diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringTestContextLoader.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringTestContextLoader.java
new file mode 100644
index 0000000..99e52e0
--- /dev/null
+++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringTestContextLoader.java
@@ -0,0 +1,551 @@
+/*
+ * 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.camel.test.junit5.spring;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Properties;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.ExtendedCamelContext;
+import org.apache.camel.api.management.JmxSystemPropertyKeys;
+import org.apache.camel.impl.engine.InterceptSendToMockEndpointStrategy;
+import org.apache.camel.processor.interceptor.DefaultDebugger;
+import org.apache.camel.spi.Breakpoint;
+import org.apache.camel.spi.Debugger;
+import org.apache.camel.spi.EventNotifier;
+import org.apache.camel.spi.PropertiesComponent;
+import org.apache.camel.spring.SpringCamelContext;
+import org.apache.camel.test.ExcludingPackageScanClassResolver;
+import org.apache.camel.test.junit5.CamelTestSupport;
+import org.apache.camel.test.junit5.spring.CamelSpringTestHelper.DoToSpringCamelContextsStrategy;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.beans.factory.support.RootBeanDefinition;
+import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.AnnotationConfigUtils;
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.test.context.MergedContextConfiguration;
+import org.springframework.test.context.support.AbstractContextLoader;
+import org.springframework.test.context.support.AbstractGenericContextLoader;
+import org.springframework.test.context.support.GenericXmlContextLoader;
+import org.springframework.util.StringUtils;
+
+import static org.apache.camel.test.spring.CamelSpringTestHelper.getAllMethods;
+
+/**
+ * Replacement for the default {@link GenericXmlContextLoader} that provides hooks for
+ * processing some class level Camel related test annotations.
+ */
+public class CamelSpringTestContextLoader extends AbstractContextLoader {
+
+ private static final Logger LOG = LoggerFactory.getLogger(CamelSpringTestContextLoader.class);
+
+ /**
+ * Modeled after the Spring implementation in {@link AbstractGenericContextLoader},
+ * this method creates and refreshes the application context while providing for
+ * processing of additional Camel specific post-refresh actions. We do not provide the
+ * pre-post hooks for customization seen in {@link AbstractGenericContextLoader} because
+ * they probably are unnecessary for 90+% of users.
+ * <p/>
+ * For some functionality, we cannot use {@link org.springframework.test.context.TestExecutionListener} because we need
+ * to both produce the desired outcome during application context loading, and also cleanup
+ * after ourselves even if the test class never executes. Thus the listeners, which
+ * only run if the application context is successfully initialized are insufficient to
+ * provide the behavior described above.
+ */
+ @Override
+ public ApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception {
+ Class<?> testClass = getTestClass();
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Loading ApplicationContext for merged context configuration [{}].", mergedConfig);
+ }
+
+ try {
+ GenericApplicationContext context = createContext(testClass, mergedConfig);
+ prepareContext(context, mergedConfig);
+ loadBeanDefinitions(context, mergedConfig);
+ return loadContext(context, testClass);
+ } finally {
+ cleanup(testClass);
+ }
+ }
+
+ /**
+ * Modeled after the Spring implementation in {@link AbstractGenericContextLoader},
+ * this method creates and refreshes the application context while providing for
+ * processing of additional Camel specific post-refresh actions. We do not provide the
+ * pre-post hooks for customization seen in {@link AbstractGenericContextLoader} because
+ * they probably are unnecessary for 90+% of users.
+ * <p/>
+ * For some functionality, we cannot use {@link org.springframework.test.context.TestExecutionListener} because we need
+ * to both produce the desired outcome during application context loading, and also cleanup
+ * after ourselves even if the test class never executes. Thus the listeners, which
+ * only run if the application context is successfully initialized are insufficient to
+ * provide the behavior described above.
+ */
+ @Override
+ public ApplicationContext loadContext(String... locations) throws Exception {
+
+ Class<?> testClass = getTestClass();
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Loading ApplicationContext for locations [" + StringUtils.arrayToCommaDelimitedString(locations) + "].");
+ }
+
+ try {
+ GenericApplicationContext context = createContext(testClass, null);
+ loadBeanDefinitions(context, locations);
+ return loadContext(context, testClass);
+ } finally {
+ cleanup(testClass);
+ }
+ }
+
+ /**
+ * Returns "<code>-context.xml</code>".
+ */
+ @Override
+ public String getResourceSuffix() {
+ return "-context.xml";
+ }
+
+ /**
+ * Performs the bulk of the Spring application context loading/customization.
+ *
+ * @param context the partially configured context. The context should have the bean definitions loaded, but nothing else.
+ * @param testClass the test class being executed
+ * @return the initialized (refreshed) Spring application context
+ *
+ * @throws Exception if there is an error during initialization/customization
+ */
+ protected ApplicationContext loadContext(GenericApplicationContext context, Class<?> testClass) throws Exception {
+
+ AnnotationConfigUtils.registerAnnotationConfigProcessors(context);
+
+ // Pre CamelContext(s) instantiation setup
+ handleDisableJmx(context, testClass);
+ handleUseOverridePropertiesWithPropertiesComponent(context, testClass);
+
+ // Temporarily disable CamelContext start while the contexts are instantiated.
+ SpringCamelContext.setNoStart(true);
+ context.refresh();
+ context.registerShutdownHook();
+ // Turn CamelContext startup back on since the context's have now been instantiated.
+ SpringCamelContext.setNoStart(false);
+
+ // Post CamelContext(s) instantiation but pre CamelContext(s) start setup
+ handleRouteCoverage(context, testClass);
+ handleProvidesBreakpoint(context, testClass);
+ handleShutdownTimeout(context, testClass);
+ handleMockEndpoints(context, testClass);
+ handleMockEndpointsAndSkip(context, testClass);
+
+ // CamelContext(s) startup
+ handleCamelContextStartup(context, testClass);
+
+ return context;
+ }
+
+ /**
+ * Cleanup/restore global state to defaults / pre-test values after the test setup
+ * is complete.
+ *
+ * @param testClass the test class being executed
+ */
+ protected void cleanup(Class<?> testClass) {
+ SpringCamelContext.setNoStart(false);
+
+ if (testClass.isAnnotationPresent(DisableJmx.class)) {
+ if (CamelSpringTestHelper.getOriginalJmxDisabled() == null) {
+ System.clearProperty(JmxSystemPropertyKeys.DISABLED);
+ } else {
+ System.setProperty(JmxSystemPropertyKeys.DISABLED,
+ CamelSpringTestHelper.getOriginalJmxDisabled());
+ }
+ }
+ }
+
+ protected void loadBeanDefinitions(GenericApplicationContext context, MergedContextConfiguration mergedConfig) {
+ (new XmlBeanDefinitionReader(context)).loadBeanDefinitions(mergedConfig.getLocations());
+ }
+
+ protected void loadBeanDefinitions(GenericApplicationContext context, String... locations) {
+ (new XmlBeanDefinitionReader(context)).loadBeanDefinitions(locations);
+ }
+
+ /**
+ * Creates and starts the Spring context while optionally starting any loaded Camel contexts.
+ *
+ * @param testClass the test class that is being executed
+ * @return the loaded Spring context
+ */
+ protected GenericApplicationContext createContext(Class<?> testClass, MergedContextConfiguration mergedConfig) {
+ ApplicationContext parentContext = null;
+ GenericApplicationContext routeExcludingContext = null;
+
+ if (mergedConfig != null) {
+ parentContext = mergedConfig.getParentApplicationContext();
+ }
+
+ if (testClass.isAnnotationPresent(ExcludeRoutes.class)) {
+ Class<?>[] excludedClasses = testClass.getAnnotation(ExcludeRoutes.class).value();
+
+ if (excludedClasses.length > 0) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Setting up package scanning excluded classes as ExcludeRoutes "
+ + "annotation was found. Excluding [" + StringUtils.arrayToCommaDelimitedString(excludedClasses) + "].");
+ }
+
+ if (parentContext == null) {
+ routeExcludingContext = new GenericApplicationContext();
+ } else {
+ routeExcludingContext = new GenericApplicationContext(parentContext);
+ }
+ routeExcludingContext.registerBeanDefinition("excludingResolver", new RootBeanDefinition(ExcludingPackageScanClassResolver.class));
+ routeExcludingContext.refresh();
+
+ ExcludingPackageScanClassResolver excludingResolver = routeExcludingContext.getBean("excludingResolver", ExcludingPackageScanClassResolver.class);
+ List<Class<?>> excluded = Arrays.asList(excludedClasses);
+ excludingResolver.setExcludedClasses(new HashSet<>(excluded));
+ } else {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Not enabling package scanning excluded classes as ExcludeRoutes "
+ + "annotation was found but no classes were excluded.");
+ }
+ }
+ }
+
+ GenericApplicationContext context;
+
+ if (routeExcludingContext != null) {
+ context = new GenericApplicationContext(routeExcludingContext);
+ } else {
+ if (parentContext != null) {
+ context = new GenericApplicationContext(parentContext);
+ } else {
+ context = new GenericApplicationContext();
+ }
+ }
+
+ return context;
+ }
+
+ /**
+ * Handles disabling of JMX on Camel contexts based on {@link DisableJmx}.
+ *
+ * @param context the initialized Spring context
+ * @param testClass the test class being executed
+ */
+ protected void handleDisableJmx(GenericApplicationContext context, Class<?> testClass) {
+ CamelSpringTestHelper.setOriginalJmxDisabledValue(System.getProperty(JmxSystemPropertyKeys.DISABLED));
+
+ if (testClass.isAnnotationPresent(DisableJmx.class)) {
+ if (testClass.getAnnotation(DisableJmx.class).value()) {
+ LOG.info("Disabling Camel JMX globally as DisableJmx annotation was found and disableJmx is set to true.");
+ System.setProperty(JmxSystemPropertyKeys.DISABLED, "true");
+ } else {
+ LOG.info("Enabling Camel JMX as DisableJmx annotation was found and disableJmx is set to false.");
+ System.clearProperty(JmxSystemPropertyKeys.DISABLED);
+ }
+ } else if (!testClass.isAnnotationPresent(EnableRouteCoverage.class)) {
+ // route coverage need JMX so do not disable it by default
+ LOG.info("Disabling Camel JMX globally for tests by default. Use the DisableJMX annotation to override the default setting.");
+ System.setProperty(JmxSystemPropertyKeys.DISABLED, "true");
+ }
+ }
+
+ /**
+ * Handles disabling of JMX on Camel contexts based on {@link DisableJmx}.
+ *
+ * @param context the initialized Spring context
+ * @param testClass the test class being executed
+ */
+ private void handleRouteCoverage(GenericApplicationContext context, Class<?> testClass) throws Exception {
+ if (testClass.isAnnotationPresent(EnableRouteCoverage.class)) {
+ System.setProperty(CamelTestSupport.ROUTE_COVERAGE_ENABLED, "true");
+
+ CamelSpringTestHelper.doToSpringCamelContexts(context, new DoToSpringCamelContextsStrategy() {
+
+ @Override
+ public void execute(String contextName, SpringCamelContext camelContext) throws Exception {
+ LOG.info("Enabling RouteCoverage");
+ EventNotifier notifier = new RouteCoverageEventNotifier(testClass.getName(), s -> getTestMethod().getName());
+ camelContext.addService(notifier, true);
+ camelContext.getManagementStrategy().addEventNotifier(notifier);
+ }
+ });
+ }
+ }
+
+ /**
+ * Handles the processing of the {@link ProvidesBreakpoint} annotation on a test class. Exists here
+ * as it is needed in
+ *
+ * @param context the initialized Spring context containing the Camel context(s) to insert breakpoints into
+ * @param testClass the test class being processed
+ *
+ * @throws Exception if there is an error processing the class
+ */
+ protected void handleProvidesBreakpoint(GenericApplicationContext context, Class<?> testClass) throws Exception {
+ Collection<Method> methods = getAllMethods(testClass);
+ final List<Breakpoint> breakpoints = new LinkedList<>();
+
+ for (Method method : methods) {
+ if (AnnotationUtils.findAnnotation(method, ProvidesBreakpoint.class) != null) {
+ Class<?>[] argTypes = method.getParameterTypes();
+ if (argTypes.length != 0) {
+ throw new IllegalArgumentException("Method [" + method.getName()
+ + "] is annotated with ProvidesBreakpoint but is not a no-argument method.");
+ } else if (!Breakpoint.class.isAssignableFrom(method.getReturnType())) {
+ throw new IllegalArgumentException("Method [" + method.getName()
+ + "] is annotated with ProvidesBreakpoint but does not return a Breakpoint.");
+ } else if (!Modifier.isStatic(method.getModifiers())) {
+ throw new IllegalArgumentException("Method [" + method.getName()
+ + "] is annotated with ProvidesBreakpoint but is not static.");
+ } else if (!Modifier.isPublic(method.getModifiers())) {
+ throw new IllegalArgumentException("Method [" + method.getName()
+ + "] is annotated with ProvidesBreakpoint but is not public.");
+ }
+
+ try {
+ breakpoints.add((Breakpoint) method.invoke(null));
+ } catch (Exception e) {
+ throw new RuntimeException("Method [" + method.getName()
+ + "] threw exception during evaluation.", e);
+ }
+ }
+ }
+
+ if (breakpoints.size() != 0) {
+ CamelSpringTestHelper.doToSpringCamelContexts(context, new DoToSpringCamelContextsStrategy() {
+
+ @Override
+ public void execute(String contextName, SpringCamelContext camelContext)
+ throws Exception {
+ Debugger debugger = camelContext.getDebugger();
+ if (debugger == null) {
+ debugger = new DefaultDebugger();
+ camelContext.setDebugger(debugger);
+ }
+
+ for (Breakpoint breakpoint : breakpoints) {
+ LOG.info("Adding Breakpoint [{}] to CamelContext with name [{}].", breakpoint, contextName);
+ debugger.addBreakpoint(breakpoint);
+ }
+ }
+ });
+ }
+ }
+
+
+ /**
+ * Handles updating shutdown timeouts on Camel contexts based on {@link ShutdownTimeout}.
+ *
+ * @param context the initialized Spring context
+ * @param testClass the test class being executed
+ */
+ protected void handleShutdownTimeout(GenericApplicationContext context, Class<?> testClass) throws Exception {
+ final int shutdownTimeout;
+ final TimeUnit shutdownTimeUnit;
+ if (testClass.isAnnotationPresent(ShutdownTimeout.class)) {
+ shutdownTimeout = testClass.getAnnotation(ShutdownTimeout.class).value();
+ shutdownTimeUnit = testClass.getAnnotation(ShutdownTimeout.class).timeUnit();
+ } else {
+ shutdownTimeout = 10;
+ shutdownTimeUnit = TimeUnit.SECONDS;
+ }
+
+ CamelSpringTestHelper.doToSpringCamelContexts(context, new DoToSpringCamelContextsStrategy() {
+
+ @Override
+ public void execute(String contextName, SpringCamelContext camelContext)
+ throws Exception {
+ LOG.info("Setting shutdown timeout to [{} {}] on CamelContext with name [{}].", shutdownTimeout, shutdownTimeUnit, contextName);
+ camelContext.getShutdownStrategy().setTimeout(shutdownTimeout);
+ camelContext.getShutdownStrategy().setTimeUnit(shutdownTimeUnit);
+ }
+ });
+ }
+
+ /**
+ * Handles auto-intercepting of endpoints with mocks based on {@link MockEndpoints}.
+ *
+ * @param context the initialized Spring context
+ * @param testClass the test class being executed
+ */
+ protected void handleMockEndpoints(GenericApplicationContext context, Class<?> testClass) throws Exception {
+ if (testClass.isAnnotationPresent(MockEndpoints.class)) {
+ final String mockEndpoints = testClass.getAnnotation(MockEndpoints.class).value();
+ CamelSpringTestHelper.doToSpringCamelContexts(context, new DoToSpringCamelContextsStrategy() {
+
+ @Override
+ public void execute(String contextName, SpringCamelContext camelContext)
+ throws Exception {
+ LOG.info("Enabling auto mocking of endpoints matching pattern [{}] on CamelContext with name [{}].", mockEndpoints, contextName);
+ camelContext.adapt(ExtendedCamelContext.class).registerEndpointCallback(new InterceptSendToMockEndpointStrategy(mockEndpoints));
+ }
+ });
+ }
+ }
+
+ /**
+ * Handles auto-intercepting of endpoints with mocks based on {@link MockEndpointsAndSkip} and skipping the
+ * original endpoint.
+ *
+ * @param context the initialized Spring context
+ * @param testClass the test class being executed
+ */
+ protected void handleMockEndpointsAndSkip(GenericApplicationContext context, Class<?> testClass) throws Exception {
+ if (testClass.isAnnotationPresent(MockEndpointsAndSkip.class)) {
+ final String mockEndpoints = testClass.getAnnotation(MockEndpointsAndSkip.class).value();
+ CamelSpringTestHelper.doToSpringCamelContexts(context, new DoToSpringCamelContextsStrategy() {
+
+ @Override
+ public void execute(String contextName, SpringCamelContext camelContext)
+ throws Exception {
+ // resovle the property place holders of the mockEndpoints
+ String mockEndpointsValue = camelContext.resolvePropertyPlaceholders(mockEndpoints);
+ LOG.info("Enabling auto mocking and skipping of endpoints matching pattern [{}] on CamelContext with name [{}].", mockEndpointsValue, contextName);
+ camelContext.adapt(ExtendedCamelContext.class).registerEndpointCallback(new InterceptSendToMockEndpointStrategy(mockEndpointsValue, true));
+ }
+ });
+ }
+ }
+
+ /**
+ * Sets property overrides for the Camel {@link org.apache.camel.component.properties.PropertiesComponent}.
+ *
+ * @param context the pre-refresh Spring context
+ * @param testClass the test class being executed
+ */
+ protected void handleUseOverridePropertiesWithPropertiesComponent(ConfigurableApplicationContext context, Class<?> testClass) throws Exception {
+ Collection<Method> methods = getAllMethods(testClass);
+ final List<Properties> properties = new LinkedList<>();
+
+ for (Method method : methods) {
+ if (AnnotationUtils.findAnnotation(method, UseOverridePropertiesWithPropertiesComponent.class) != null) {
+ Class<?>[] argTypes = method.getParameterTypes();
+ if (argTypes.length > 0) {
+ throw new IllegalArgumentException("Method [" + method.getName()
+ + "] is annotated with UseOverridePropertiesWithPropertiesComponent but is not a no-argument method.");
+ } else if (!Properties.class.isAssignableFrom(method.getReturnType())) {
+ throw new IllegalArgumentException("Method [" + method.getName()
+ + "] is annotated with UseOverridePropertiesWithPropertiesComponent but does not return a java.util.Properties.");
+ } else if (!Modifier.isStatic(method.getModifiers())) {
+ throw new IllegalArgumentException("Method [" + method.getName()
+ + "] is annotated with UseOverridePropertiesWithPropertiesComponent but is not static.");
+ } else if (!Modifier.isPublic(method.getModifiers())) {
+ throw new IllegalArgumentException("Method [" + method.getName()
+ + "] is annotated with UseOverridePropertiesWithPropertiesComponent but is not public.");
+ }
+
+ try {
+ properties.add((Properties) method.invoke(null));
+ } catch (Exception e) {
+ throw new RuntimeException("Method [" + method.getName()
+ + "] threw exception during evaluation.", e);
+ }
+ }
+ }
+
+ Properties extra = new Properties();
+ for (Properties prop : properties) {
+ extra.putAll(prop);
+ }
+
+ if (!extra.isEmpty()) {
+ context.addBeanFactoryPostProcessor(beanFactory -> beanFactory.addBeanPostProcessor(new BeanPostProcessor() {
+ @Override
+ public Object postProcessAfterInitialization(Object bean, String beanName) {
+ if (bean instanceof CamelContext) {
+ CamelContext camelContext = (CamelContext) bean;
+ PropertiesComponent pc = camelContext.getPropertiesComponent(true);
+ LOG.info("Using {} properties to override any existing properties on the PropertiesComponent on CamelContext with name [{}].", extra.size(), camelContext.getName());
+ pc.setOverrideProperties(extra);
+ }
+ return bean;
+ }
+ }));
+ }
+ }
+
+ /**
+ * Handles starting of Camel contexts based on {@link UseAdviceWith} and other state in the JVM.
+ *
+ * @param context the initialized Spring context
+ * @param testClass the test class being executed
+ */
+ protected void handleCamelContextStartup(GenericApplicationContext context, Class<?> testClass) throws Exception {
+ boolean skip = "true".equalsIgnoreCase(System.getProperty("skipStartingCamelContext"));
+ if (skip) {
+ LOG.info("Skipping starting CamelContext(s) as system property skipStartingCamelContext is set to be true.");
+ } else if (testClass.isAnnotationPresent(UseAdviceWith.class)) {
+ if (testClass.getAnnotation(UseAdviceWith.class).value()) {
+ LOG.info("Skipping starting CamelContext(s) as UseAdviceWith annotation was found and isUseAdviceWith is set to true.");
+ skip = true;
+ } else {
+ LOG.info("Starting CamelContext(s) as UseAdviceWith annotation was found, but isUseAdviceWith is set to false.");
+ skip = false;
+ }
+ }
+
+ if (!skip) {
+ CamelSpringTestHelper.doToSpringCamelContexts(context, new DoToSpringCamelContextsStrategy() {
+
+ @Override
+ public void execute(String contextName,
+ SpringCamelContext camelContext) throws Exception {
+ LOG.info("Starting CamelContext with name [{}].", contextName);
+ camelContext.start();
+ }
+ });
+ }
+ }
+
+ /**
+ * Returns the class under test in order to enable inspection of annotations while the
+ * Spring context is being created.
+ *
+ * @return the test class that is being executed
+ * @see CamelSpringTestHelper
+ */
+ protected Class<?> getTestClass() {
+ return CamelSpringTestHelper.getTestClass();
+ }
+
+ /**
+ * Returns the test method under test.
+ *
+ * @return the method that is being executed
+ * @see CamelSpringTestHelper
+ */
+ protected Method getTestMethod() {
+ return CamelSpringTestHelper.getTestMethod();
+ }
+}
diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringTestContextLoaderTestExecutionListener.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringTestContextLoaderTestExecutionListener.java
new file mode 100644
index 0000000..a749104
--- /dev/null
+++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringTestContextLoaderTestExecutionListener.java
@@ -0,0 +1,50 @@
+/*
+ * 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.camel.test.junit5.spring;
+
+import org.springframework.core.Ordered;
+import org.springframework.test.context.TestContext;
+import org.springframework.test.context.support.AbstractTestExecutionListener;
+
+/**
+ * Helper for {@link CamelSpringTestContextLoader} that sets the test class state
+ * in {@link CamelSpringTestHelper} almost immediately before the loader initializes
+ * the Spring context.
+ * <p/>
+ * Implemented as a listener as the state can be set on a {@code ThreadLocal} and we are pretty sure
+ * that the same thread will be used to initialize the Spring context.
+ */
+public class CamelSpringTestContextLoaderTestExecutionListener extends AbstractTestExecutionListener {
+
+ /**
+ * The default implementation returns {@link org.springframework.core.Ordered#LOWEST_PRECEDENCE},
+ * thereby ensuring that custom listeners are ordered after default
+ * listeners supplied by the framework. Can be overridden by subclasses
+ * as necessary.
+ */
+ @Override
+ public int getOrder() {
+ //set Camel first
+ return Ordered.HIGHEST_PRECEDENCE;
+ }
+
+ @Override
+ public void prepareTestInstance(TestContext testContext) throws Exception {
+ CamelSpringTestHelper.setTestClass(testContext.getTestClass());
+ CamelSpringTestHelper.setTestContext(testContext);
+ }
+}
diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringTestHelper.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringTestHelper.java
new file mode 100644
index 0000000..a9ed288
--- /dev/null
+++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringTestHelper.java
@@ -0,0 +1,109 @@
+/*
+ * 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.camel.test.junit5.spring;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import org.apache.camel.spring.SpringCamelContext;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.test.context.TestContext;
+
+/**
+ * Helper that provides state information across the levels of Spring Test that do not expose the
+ * necessary context/state for integration of Camel testing features into Spring test. Also
+ * provides utility methods.
+ * <p/>
+ * Note that this class makes use of {@link ThreadLocal}s to maintain some state. It is imperative
+ * that the state setters and getters are accessed within the scope of a single thread in order
+ * for this class to work right.
+ */
+public final class CamelSpringTestHelper {
+
+ private static ThreadLocal<String> originalJmxDisabledValue = new ThreadLocal<>();
+ private static ThreadLocal<Class<?>> testClazz = new ThreadLocal<>();
+ private static ThreadLocal<TestContext> testContext = new ThreadLocal<>();
+
+ private CamelSpringTestHelper() {
+ }
+
+ public static String getOriginalJmxDisabled() {
+ return originalJmxDisabledValue.get();
+ }
+
+ public static void setOriginalJmxDisabledValue(String originalValue) {
+ originalJmxDisabledValue.set(originalValue);
+ }
+
+ public static Class<?> getTestClass() {
+ return testClazz.get();
+ }
+
+ public static void setTestClass(Class<?> testClass) {
+ testClazz.set(testClass);
+ }
+
+ public static Method getTestMethod() {
+ return testContext.get().getTestMethod();
+ }
+
+ public static void setTestContext(TestContext context) {
+ testContext.set(context);
+ }
+
+ /**
+ * Returns all methods defined in {@code clazz} and its superclasses/interfaces.
+ */
+ public static Collection<Method> getAllMethods(Class<?> clazz) {
+ Set<Method> methods = new LinkedHashSet<>();
+ Class<?> currentClass = clazz;
+
+ while (currentClass != null) {
+ methods.addAll(Arrays.asList(clazz.getMethods()));
+ currentClass = currentClass.getSuperclass();
+ }
+
+ return methods;
+ }
+
+ /**
+ * Executes {@code strategy} against all {@link SpringCamelContext}s found in the Spring context.
+ * This method reduces the amount of repeated find and loop code throughout this class.
+ *
+ * @param context the Spring context to search
+ * @param strategy the strategy to execute against the found {@link SpringCamelContext}s
+ *
+ * @throws Exception if there is an error executing any of the strategies
+ */
+ public static void doToSpringCamelContexts(ApplicationContext context, DoToSpringCamelContextsStrategy strategy) throws Exception {
+ Map<String, SpringCamelContext> contexts = context.getBeansOfType(SpringCamelContext.class);
+
+ for (Entry<String, SpringCamelContext> entry : contexts.entrySet()) {
+ strategy.execute(entry.getKey(), entry.getValue());
+ }
+ }
+
+ public interface DoToSpringCamelContextsStrategy {
+ void execute(String contextName, SpringCamelContext camelContext) throws Exception;
+ }
+}
diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringTestSupport.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringTestSupport.java
new file mode 100644
index 0000000..839f675
--- /dev/null
+++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringTestSupport.java
@@ -0,0 +1,212 @@
+/*
+ * 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.camel.test.junit5.spring;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.ExtendedCamelContext;
+import org.apache.camel.spring.SpringCamelContext;
+import org.apache.camel.test.ExcludingPackageScanClassResolver;
+import org.apache.camel.test.junit5.CamelTestSupport;
+import org.apache.camel.util.IOHelper;
+import org.apache.camel.util.ObjectHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.springframework.beans.factory.support.RootBeanDefinition;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.support.AbstractApplicationContext;
+import org.springframework.context.support.GenericApplicationContext;
+
+/**
+ * Base test-class for classic Spring application such as standalone, web applications.
+ * Do <tt>not</tt> use this class for Spring Boot testing, instead use <code>@RunWith(CamelSpringBootRunner.class)</code>.
+ */
+public abstract class CamelSpringTestSupport extends CamelTestSupport {
+ protected static ThreadLocal<AbstractApplicationContext> threadAppContext = new ThreadLocal<>();
+ protected static Object lock = new Object();
+
+ protected AbstractApplicationContext applicationContext;
+ protected abstract AbstractApplicationContext createApplicationContext();
+
+ @Override
+ public void postProcessTest() throws Exception {
+ if (isCreateCamelContextPerClass()) {
+ applicationContext = threadAppContext.get();
+ }
+ super.postProcessTest();
+ }
+
+ @Override
+ public void doPreSetup() throws Exception {
+ if (!"true".equalsIgnoreCase(System.getProperty("skipStartingCamelContext"))) {
+ // tell camel-spring it should not trigger starting CamelContext, since we do that later
+ // after we are finished setting up the unit test
+ synchronized (lock) {
+ SpringCamelContext.setNoStart(true);
+ if (isCreateCamelContextPerClass()) {
+ applicationContext = threadAppContext.get();
+ if (applicationContext == null) {
+ applicationContext = doCreateApplicationContext();
+ threadAppContext.set(applicationContext);
+ }
+ } else {
+ applicationContext = doCreateApplicationContext();
+ }
+ SpringCamelContext.setNoStart(false);
+ }
+ } else {
+ log.info("Skipping starting CamelContext as system property skipStartingCamelContext is set to be true.");
+ }
+ }
+
+ private AbstractApplicationContext doCreateApplicationContext() {
+ AbstractApplicationContext context = createApplicationContext();
+ Assertions.assertNotNull(context, "Should have created a valid Spring application context");
+
+ String[] profiles = activeProfiles();
+ if (profiles != null && profiles.length > 0) {
+ // the context must not be active
+ if (context.isActive()) {
+ throw new IllegalStateException("Cannot active profiles: " + Arrays.asList(profiles) + " on active Spring application context: " + context
+ + ". The code in your createApplicationContext() method should be adjusted to create the application context with refresh = false as parameter");
+ }
+ log.info("Spring activating profiles: {}", Arrays.asList(profiles));
+ context.getEnvironment().setActiveProfiles(profiles);
+ }
+
+ // ensure the context has been refreshed at least once
+ if (!context.isActive()) {
+ context.refresh();
+ }
+
+ return context;
+ }
+
+ @Override
+ @AfterEach
+ public void tearDown() throws Exception {
+ super.tearDown();
+
+ if (!isCreateCamelContextPerClass()) {
+ IOHelper.close(applicationContext);
+ applicationContext = null;
+ }
+ }
+
+ @Override
+ public void doPostTearDown() throws Exception {
+ super.doPostTearDown();
+
+ if (threadAppContext.get() != null) {
+ IOHelper.close(threadAppContext.get());
+ threadAppContext.remove();
+ }
+ }
+
+ /**
+ * Create a parent context that initializes a
+ * {@link org.apache.camel.spi.PackageScanClassResolver} to exclude a set of given classes from
+ * being resolved. Typically this is used at test time to exclude certain routes,
+ * which might otherwise be just noisy, from being discovered and initialized.
+ * <p/>
+ * To use this filtering mechanism it is necessary to provide the
+ * {@link org.springframework.context.ApplicationContext} returned from here as the parent context to
+ * your test context e.g.
+ *
+ * <pre>
+ * protected AbstractXmlApplicationContext createApplicationContext() {
+ * return new ClassPathXmlApplicationContext(new String[] {"test-context.xml"}, getRouteExcludingApplicationContext());
+ * }
+ * </pre>
+ *
+ * This will, in turn, call the template methods <code>excludedRoutes</code>
+ * and <code>excludedRoute</code> to determine the classes to be excluded from scanning.
+ *
+ * @return ApplicationContext a parent {@link org.springframework.context.ApplicationContext} configured
+ * to exclude certain classes from package scanning
+ */
+ protected ApplicationContext getRouteExcludingApplicationContext() {
+ GenericApplicationContext routeExcludingContext = new GenericApplicationContext();
+ routeExcludingContext.registerBeanDefinition("excludingResolver", new RootBeanDefinition(ExcludingPackageScanClassResolver.class));
+ routeExcludingContext.refresh();
+
+ ExcludingPackageScanClassResolver excludingResolver = routeExcludingContext.getBean("excludingResolver", ExcludingPackageScanClassResolver.class);
+ List<Class<?>> excluded = Arrays.asList(excludeRoutes());
+ excludingResolver.setExcludedClasses(new HashSet<>(excluded));
+
+ return routeExcludingContext;
+ }
+
+ /**
+ * Template method used to exclude {@link org.apache.camel.Route} from the test time context
+ * route scanning
+ *
+ * @return Class[] the classes to be excluded from test time context route scanning
+ */
+ protected Class<?>[] excludeRoutes() {
+ Class<?> excludedRoute = excludeRoute();
+ return excludedRoute != null ? new Class[] {excludedRoute} : new Class[0];
+ }
+
+ /**
+ * Template method used to exclude a {@link org.apache.camel.Route} from the test camel context
+ */
+ protected Class<?> excludeRoute() {
+ return null;
+ }
+
+ /**
+ * Looks up the mandatory spring bean of the given name and type, failing if
+ * it is not present or the correct type
+ */
+ public <T> T getMandatoryBean(Class<T> type, String name) {
+ Object value = applicationContext.getBean(name);
+ Assertions.assertNotNull(value, "No spring bean found for name <" + name + ">");
+ if (type.isInstance(value)) {
+ return type.cast(value);
+ } else {
+ Assertions.fail("Spring bean <" + name + "> is not an instanceof " + type.getName() + " but is of type " + ObjectHelper.className(value));
+ return null;
+ }
+ }
+
+ /**
+ * Which active profiles should be used.
+ * <p/>
+ * <b>Important:</b> When using active profiles, then the code in {@link #createApplicationContext()} should create
+ * the Spring {@link org.springframework.context.support.AbstractApplicationContext} without refreshing. For example creating an
+ * {@link org.springframework.context.support.ClassPathXmlApplicationContext} you would need to pass in
+ * <tt>false</tt> in the refresh parameter, in the constructor.
+ * Camel will thrown an {@link IllegalStateException} if this is not correct stating this problem.
+ * The reason is that we cannot active profiles <b>after</b> a Spring application context has already
+ * been refreshed, and is active.
+ *
+ * @return an array of active profiles to use, use <tt>null</tt> to not use any active profiles.
+ */
+ protected String[] activeProfiles() {
+ return null;
+ }
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ // don't start the springCamelContext if we
+ return SpringCamelContext.springCamelContext(applicationContext, false);
+ }
+}
diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelTestContextBootstrapper.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelTestContextBootstrapper.java
new file mode 100644
index 0000000..aef6e19
--- /dev/null
+++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelTestContextBootstrapper.java
@@ -0,0 +1,31 @@
+/*
+ * 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.camel.test.junit5.spring;
+
+import org.springframework.test.context.ContextLoader;
+import org.springframework.test.context.support.DefaultTestContextBootstrapper;
+
+/**
+ * To bootstrap Camel for testing with Spring 4.1 onwards.
+ */
+public class CamelTestContextBootstrapper extends DefaultTestContextBootstrapper {
+
+ @Override
+ protected Class<? extends ContextLoader> getDefaultContextLoaderClass(Class<?> testClass) {
+ return CamelSpringTestContextLoader.class;
+ }
+}
diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/DisableJmx.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/DisableJmx.java
new file mode 100644
index 0000000..b3f44c1
--- /dev/null
+++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/DisableJmx.java
@@ -0,0 +1,43 @@
+/*
+ * 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.camel.test.junit5.spring;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates if JMX should be globally disabled in the {@code CamelContext}s that are bootstrapped
+ * during the test through the use of Spring Test loaded application contexts. Note that the
+ * presence of this annotation will result in the manipulation of System Properties that
+ * will affect Camel contexts constructed outside of the Spring Test loaded application contexts.
+ */
+@Documented
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE})
+public @interface DisableJmx {
+
+ /**
+ * Whether the test annotated with this annotation should be run with JMX disabled in Camel.
+ * Defaults to {@code true}.
+ */
+ boolean value() default true;
+}
diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/DisableJmxTestExecutionListener.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/DisableJmxTestExecutionListener.java
new file mode 100644
index 0000000..f3aaee6
--- /dev/null
+++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/DisableJmxTestExecutionListener.java
@@ -0,0 +1,39 @@
+/*
+ * 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.camel.test.junit5.spring;
+
+import org.apache.camel.api.management.JmxSystemPropertyKeys;
+import org.springframework.test.context.TestContext;
+import org.springframework.test.context.support.AbstractTestExecutionListener;
+
+/**
+ * Provides reset to pre-test state behavior for global enable/disable of JMX
+ * support in Camel through the use of {@link DisableJmx}.
+ * Tries to ensure that the pre-test value is restored.
+ */
+public class DisableJmxTestExecutionListener extends AbstractTestExecutionListener {
+
+ @Override
+ public void afterTestClass(TestContext testContext) throws Exception {
+ if (CamelSpringTestHelper.getOriginalJmxDisabled() == null) {
+ System.clearProperty(JmxSystemPropertyKeys.DISABLED);
+ } else {
+ System.setProperty(JmxSystemPropertyKeys.DISABLED, CamelSpringTestHelper.getOriginalJmxDisabled());
+ }
+ }
+
+}
diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/EnableRouteCoverage.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/EnableRouteCoverage.java
new file mode 100644
index 0000000..9b846b9
--- /dev/null
+++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/EnableRouteCoverage.java
@@ -0,0 +1,41 @@
+/*
+ * 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.camel.test.junit5.spring;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Enables dumping route coverage statistic.
+ * The route coverage status is written as xml files in the <tt>target/camel-route-coverage</tt> directory after the test has finished.
+ * <p/>
+ * This allows tooling or manual inspection of the stats, so you can generate a route trace diagram of which EIPs
+ * have been in use and which have not. Similar concepts as a code coverage report.
+ * <p/>
+ * You can also turn on route coverage globally via setting JVM system property <tt>CamelTestRouteCoverage=true</tt>.
+ */
+@Documented
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE})
+public @interface EnableRouteCoverage {
+
+}
diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/ExcludeRoutes.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/ExcludeRoutes.java
new file mode 100644
index 0000000..eab25f0
--- /dev/null
+++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/ExcludeRoutes.java
@@ -0,0 +1,44 @@
+/*
+ * 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.camel.test.junit5.spring;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.apache.camel.RoutesBuilder;
+
+/**
+ * Indicates if certain route builder classes should be excluded from discovery.
+ * Initializes a {@link org.apache.camel.spi.PackageScanClassResolver} to exclude a set of given
+ * classes from being resolved. Typically this is used at test time to exclude certain routes,
+ * which might otherwise be noisy, from being discovered and initialized.
+ */
+@Documented
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE})
+public @interface ExcludeRoutes {
+
+ /**
+ * The classes to exclude from resolution when using package scanning.
+ */
+ Class<? extends RoutesBuilder>[] value() default {};
+}
diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/MockEndpoints.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/MockEndpoints.java
new file mode 100644
index 0000000..2e9215a
--- /dev/null
+++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/MockEndpoints.java
@@ -0,0 +1,43 @@
+/*
+ * 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.camel.test.junit5.spring;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.apache.camel.impl.engine.InterceptSendToMockEndpointStrategy;
+
+/**
+ * Triggers the auto-mocking of endpoints whose URIs match the provided filter. The default
+ * filter is "*" which matches all endpoints. See {@link InterceptSendToMockEndpointStrategy} for
+ * more details on the registration of the mock endpoints.
+ */
+@Documented
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE})
+public @interface MockEndpoints {
+
+ /**
+ * The pattern to use for matching endpoints to enable mocking on.
+ */
+ String value() default "*";
+}
diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/MockEndpointsAndSkip.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/MockEndpointsAndSkip.java
new file mode 100644
index 0000000..4e88a3c
--- /dev/null
+++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/MockEndpointsAndSkip.java
@@ -0,0 +1,43 @@
+/*
+ * 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.camel.test.junit5.spring;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.apache.camel.impl.engine.InterceptSendToMockEndpointStrategy;
+
+/**
+ * Triggers the auto-mocking of endpoints whose URIs match the provided filter with the added provision
+ * that the endpoints are also skipped. The default filter is "*" which matches all endpoints.
+ * See {@link InterceptSendToMockEndpointStrategy} for more details on the registration of the mock endpoints.
+ */
+@Documented
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE})
+public @interface MockEndpointsAndSkip {
+
+ /**
+ * The pattern to use for matching endpoints to enable mocking on.
+ */
+ String value() default "*";
+}
diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/ProvidesBreakpoint.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/ProvidesBreakpoint.java
new file mode 100644
index 0000000..088b313
--- /dev/null
+++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/ProvidesBreakpoint.java
@@ -0,0 +1,36 @@
+/*
+ * 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.camel.test.junit5.spring;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.apache.camel.spi.Breakpoint;
+
+/**
+ * Indicates that the annotated method returns a {@link Breakpoint} for use in the test. Useful for intercepting
+ * traffic to all endpoints or simply for setting a break point in an IDE for debugging. The method must
+ * be {@code public}, {@code static}, take no arguments, and return {@link Breakpoint}.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD})
+public @interface ProvidesBreakpoint {
+}
diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/RouteCoverageDumper.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/RouteCoverageDumper.java
new file mode 100644
index 0000000..11b11b2
--- /dev/null
+++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/RouteCoverageDumper.java
@@ -0,0 +1,82 @@
+/*
+ * 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.camel.test.junit5.spring;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.api.management.ManagedCamelContext;
+import org.apache.camel.api.management.mbean.ManagedCamelContextMBean;
+import org.apache.camel.util.IOHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Helper to dump route coverage when using {@link EnableRouteCoverage}.
+ */
+public final class RouteCoverageDumper {
+
+ private static final Logger LOG = LoggerFactory.getLogger(RouteCoverageDumper.class);
+
+ private RouteCoverageDumper() {
+ }
+
+ public static void dumpRouteCoverage(CamelContext context, String testClassName, String testName) {
+ try {
+ String dir = "target/camel-route-coverage";
+ String name = testClassName + "-" + testName + ".xml";
+
+ ManagedCamelContextMBean managedCamelContext = context.getExtension(ManagedCamelContext.class).getManagedCamelContext();
+ if (managedCamelContext == null) {
+ LOG.warn("Cannot dump route coverage to file as JMX is not enabled. Override useJmx() method to enable JMX in the unit test classes.");
+ } else {
+ String xml = managedCamelContext.dumpRoutesCoverageAsXml();
+ String combined = "<camelRouteCoverage>\n" + gatherTestDetailsAsXml(testClassName, testName) + xml + "\n</camelRouteCoverage>";
+
+ File file = new File(dir);
+ // ensure dir exists
+ file.mkdirs();
+ file = new File(dir, name);
+
+ LOG.info("Dumping route coverage to file: " + file);
+ InputStream is = new ByteArrayInputStream(combined.getBytes());
+ OutputStream os = new FileOutputStream(file, false);
+ IOHelper.copyAndCloseInput(is, os);
+ IOHelper.close(os);
+ }
+ } catch (Exception e) {
+ LOG.warn("Error during dumping route coverage statistic. This exception is ignored.", e);
+ }
+
+ }
+
+ /**
+ * Gathers test details as xml
+ */
+ private static String gatherTestDetailsAsXml(String testClassName, String testName) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("<test>\n");
+ sb.append(" <class>").append(testClassName).append("</class>\n");
+ sb.append(" <method>").append(testName).append("</method>\n");
+ sb.append("</test>\n");
+ return sb.toString();
+ }
+}
diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/RouteCoverageEventNotifier.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/RouteCoverageEventNotifier.java
new file mode 100644
index 0000000..7d7df3d
--- /dev/null
+++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/RouteCoverageEventNotifier.java
@@ -0,0 +1,51 @@
+/*
+ * 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.camel.test.junit5.spring;
+
+import java.util.function.Function;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.spi.CamelEvent;
+import org.apache.camel.spi.CamelEvent.CamelContextEvent;
+import org.apache.camel.spi.CamelEvent.CamelContextStoppingEvent;
+import org.apache.camel.support.EventNotifierSupport;
+
+public class RouteCoverageEventNotifier extends EventNotifierSupport {
+
+ private final String testClassName;
+ private final Function testMethodName;
+
+ public RouteCoverageEventNotifier(String testClassName, Function testMethodName) {
+ this.testClassName = testClassName;
+ this.testMethodName = testMethodName;
+ setIgnoreCamelContextEvents(false);
+ setIgnoreExchangeEvents(true);
+ }
+
+ @Override
+ public boolean isEnabled(CamelEvent event) {
+ return event instanceof CamelContextStoppingEvent;
+ }
+
+ @Override
+ public void notify(CamelEvent event) throws Exception {
+ CamelContext context = ((CamelContextStoppingEvent) event).getContext();
+ String testName = (String) testMethodName.apply(this);
+ RouteCoverageDumper.dumpRouteCoverage(context, testClassName, testName);
+ }
+
+}
diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/ShutdownTimeout.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/ShutdownTimeout.java
new file mode 100644
index 0000000..8e35d58
--- /dev/null
+++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/ShutdownTimeout.java
@@ -0,0 +1,49 @@
+/*
+ * 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.camel.test.junit5.spring;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Indicates to set the shutdown timeout of all {@code CamelContext}s instantiated through the
+ * use of Spring Test loaded application contexts. If no annotation is used, the timeout is
+ * automatically reduced to 10 seconds by the test framework. If the annotation is present the
+ * shutdown timeout is set based on the value of {@link #value()}.
+ */
+@Documented
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE})
+public @interface ShutdownTimeout {
+
+ /**
+ * The shutdown timeout to set on the {@code CamelContext}(s).
+ * Defaults to {@code 10} seconds.
+ */
+ int value() default 10;
+
+ /**
+ * The time unit that {@link #value()} is in.
+ */
+ TimeUnit timeUnit() default TimeUnit.SECONDS;
+}
diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/StopWatchTestExecutionListener.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/StopWatchTestExecutionListener.java
new file mode 100644
index 0000000..535a7dc
--- /dev/null
+++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/StopWatchTestExecutionListener.java
@@ -0,0 +1,62 @@
+/*
+ * 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.camel.test.junit5.spring;
+
+import org.apache.camel.util.StopWatch;
+import org.apache.camel.util.TimeUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.test.context.TestContext;
+import org.springframework.test.context.support.AbstractTestExecutionListener;
+
+/**
+ * An execution listener that simulates the timing output built in to {@link org.apache.camel.test.junit4.CamelTestSupport}.
+ */
+public class StopWatchTestExecutionListener extends AbstractTestExecutionListener {
+
+ protected static ThreadLocal<StopWatch> threadStopWatch = new ThreadLocal<>();
+
+ /**
+ * Exists primarily for testing purposes, but allows for access to the underlying stop watch instance for a test.
+ */
+ public static StopWatch getStopWatch() {
+ return threadStopWatch.get();
+ }
+
+ @Override
+ public void beforeTestMethod(TestContext testContext) throws Exception {
+ StopWatch stopWatch = new StopWatch();
+ threadStopWatch.set(stopWatch);
+ }
+
+ @Override
+ public void afterTestMethod(TestContext testContext) throws Exception {
+ StopWatch watch = threadStopWatch.get();
+ if (watch != null) {
+ long time = watch.taken();
+ Logger log = LoggerFactory.getLogger(testContext.getTestClass());
+
+ log.info("********************************************************************************");
+ log.info("Testing done: " + testContext.getTestMethod().getName() + "(" + testContext.getTestClass().getName() + ")");
+ log.info("Took: " + TimeUtils.printDuration(time) + " (" + time + " millis)");
+ log.info("********************************************************************************");
+
+ threadStopWatch.remove();
+ }
+ }
+
+}
diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/UseAdviceWith.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/UseAdviceWith.java
new file mode 100644
index 0000000..9e09deb
--- /dev/null
+++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/UseAdviceWith.java
@@ -0,0 +1,49 @@
+/*
+ * 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.camel.test.junit5.spring;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.apache.camel.CamelContext;
+
+/**
+ * Indicates the use of {@code adviceWith()} within the test class. If a class is annotated with
+ * this annotation and {@link UseAdviceWith#value()} returns true, any
+ * {@code CamelContext}s bootstrapped during the test through the use of Spring Test loaded
+ * application contexts will not be started automatically. The test author is responsible for
+ * injecting the Camel contexts into the test and executing {@link CamelContext#start()} on them
+ * at the appropriate time after any advice has been applied to the routes in the Camel context(s).
+ */
+@Documented
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE})
+public @interface UseAdviceWith {
+
+ /**
+ * Whether the test annotated with this annotation should be treated as if
+ * {@code adviceWith()} is in use in the test and the Camel contexts should not be started
+ * automatically.
+ * Defaults to {@code true}.
+ */
+ boolean value() default true;
+}
diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/UseOverridePropertiesWithPropertiesComponent.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/UseOverridePropertiesWithPropertiesComponent.java
new file mode 100644
index 0000000..89a71a5
--- /dev/null
+++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/UseOverridePropertiesWithPropertiesComponent.java
@@ -0,0 +1,34 @@
+/*
+ * 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.camel.test.junit5.spring;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates that the annotated method returns a {@link java.util.Properties} for use in the test, and that
+ * those properties override any existing properties configured on the {@link org.apache.camel.component.properties.PropertiesComponent}.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD})
+public @interface UseOverridePropertiesWithPropertiesComponent {
+
+}
diff --git a/components/camel-testcontainers-spring/src/main/java/org/apache/camel/test/junit5/testcontainers/spring/ContainerAwareSpringTestSupport.java b/components/camel-testcontainers-spring/src/main/java/org/apache/camel/test/junit5/testcontainers/spring/ContainerAwareSpringTestSupport.java
new file mode 100644
index 0000000..74130af
--- /dev/null
+++ b/components/camel-testcontainers-spring/src/main/java/org/apache/camel/test/junit5/testcontainers/spring/ContainerAwareSpringTestSupport.java
@@ -0,0 +1,112 @@
+/*
+ * 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.camel.test.junit5.testcontainers.spring;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.component.properties.PropertiesComponent;
+import org.apache.camel.test.junit5.spring.CamelSpringTestSupport;
+import org.apache.camel.test.testcontainers.ContainerPropertiesFunction;
+import org.apache.camel.test.testcontainers.Containers;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.Network;
+
+public abstract class ContainerAwareSpringTestSupport extends CamelSpringTestSupport {
+ private List<GenericContainer<?>> containers = new CopyOnWriteArrayList<>();
+
+ // ******************
+ // Setup
+ // ******************
+
+ @Override
+ protected void setupResources() throws Exception {
+ super.setupResources();
+
+ containers.clear();
+ containers.addAll(createContainers());
+
+ final Network network = containerNetwork();
+ final long timeout = containersStartupTimeout();
+
+ Containers.start(containers, network, timeout);
+ }
+
+ @Override
+ protected void cleanupResources() throws Exception {
+ super.cleanupResources();
+
+ Containers.stop(containers, containerShutdownTimeout());
+ }
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ final CamelContext context = super.createCamelContext();
+ final PropertiesComponent pc = context.getComponent("properties", PropertiesComponent.class);
+
+ pc.addFunction(new ContainerPropertiesFunction(containers));
+
+ return context;
+ }
+
+ // ******************
+ // Containers set-up
+ // ******************
+
+ protected GenericContainer<?> createContainer() {
+ return null;
+ }
+
+ protected List<GenericContainer<?>> createContainers() {
+ GenericContainer<?> container = createContainer();
+
+ return container == null
+ ? Collections.emptyList()
+ : Collections.singletonList(container);
+ }
+
+ protected long containersStartupTimeout() {
+ return TimeUnit.MINUTES.toSeconds(1);
+ }
+
+ protected long containerShutdownTimeout() {
+ return TimeUnit.MINUTES.toSeconds(1);
+ }
+
+ protected Network containerNetwork() {
+ return null;
+ }
+
+ // ******************
+ // Helpers
+ // ******************
+
+ protected GenericContainer<?> getContainer(String containerName) {
+ return Containers.lookup(containers, containerName);
+ }
+
+ protected String getContainerHost(String containerName) {
+ return getContainer(containerName).getContainerIpAddress();
+ }
+
+ protected int getContainerPort(String containerName, int originalPort) {
+ return getContainer(containerName).getMappedPort(originalPort);
+ }
+}
diff --git a/components/camel-testcontainers-spring/src/test/java/org/apache/camel/test/junit5/testcontainers/spring/ContainerAwareSpringTestSupportIT.java b/components/camel-testcontainers-spring/src/test/java/org/apache/camel/test/junit5/testcontainers/spring/ContainerAwareSpringTestSupportIT.java
new file mode 100644
index 0000000..fef4342
--- /dev/null
+++ b/components/camel-testcontainers-spring/src/test/java/org/apache/camel/test/junit5/testcontainers/spring/ContainerAwareSpringTestSupportIT.java
@@ -0,0 +1,61 @@
+/*
+ * 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.camel.test.junit5.testcontainers.spring;
+
+import org.apache.camel.test.testcontainers.Wait;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.support.AbstractApplicationContext;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+import org.testcontainers.containers.GenericContainer;
+
+public class ContainerAwareSpringTestSupportIT extends ContainerAwareSpringTestSupport {
+ @Override
+ protected AbstractApplicationContext createApplicationContext() {
+ return new ClassPathXmlApplicationContext("org/apache/camel/test/testcontainers/spring/ContainerAwareSpringTestSupportTest.xml");
+ }
+
+ @Test
+ public void testPropertyPlaceholders() throws Exception {
+ final GenericContainer<?> container = getContainer("myconsul");
+
+ final String host = context.resolvePropertyPlaceholders("{{container:host:myconsul}}");
+ Assertions.assertThat(host).isEqualTo(container.getContainerIpAddress());
+
+ final String port = context.resolvePropertyPlaceholders("{{container:port:8500@myconsul}}");
+ Assertions.assertThat(port).isEqualTo("" + container.getMappedPort(8500));
+ }
+
+ @Override
+ protected GenericContainer<?> createContainer() {
+ return new GenericContainer("consul:1.5.1")
+ .withNetworkAliases("myconsul")
+ .withExposedPorts(8500)
+ .waitingFor(Wait.forLogMessageContaining("Synced node info", 1))
+ .withCommand(
+ "agent",
+ "-dev",
+ "-server",
+ "-bootstrap",
+ "-client",
+ "0.0.0.0",
+ "-log-level",
+ "trace"
+ );
+ }
+
+}