Skip to content

Commit

Permalink
Allow to write integration tests with TestContainer
Browse files Browse the repository at this point in the history
  • Loading branch information
essobedo committed May 2, 2024
1 parent caf1b91 commit 75f4c1d
Show file tree
Hide file tree
Showing 14 changed files with 477 additions and 222 deletions.
17 changes: 16 additions & 1 deletion tests/camel-integration-test/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,11 @@
org.apache.karaf.camel.itests;version=${project.version},
</Export-Package>
<Import-Package>
org.apache.karaf.itests,org.ops4j.pax.exam,org.osgi.framework,org.junit*,
org.apache.karaf.itests,
org.ops4j.pax.exam,
org.ops4j.pax.exam.options,
org.osgi.framework,
org.junit*,
org.apache.camel*;${camel.osgi.import.camel.version},
org.apache.karaf.features,
org.ops4j.pax.swissbox.tracker,
Expand Down Expand Up @@ -121,6 +125,11 @@
<groupId>org.ops4j.pax.exam</groupId>
<artifactId>pax-exam-container-karaf</artifactId>
</dependency>
<dependency>
<groupId>org.ops4j.pax.exam</groupId>
<artifactId>pax-exam-junit4</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
Expand Down Expand Up @@ -178,5 +187,11 @@
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>${testcontainers-version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.util.stream.Stream;

import org.apache.karaf.itests.KarafTestSupport;
import org.junit.Assert;
Expand All @@ -39,6 +38,7 @@
import org.osgi.framework.BundleException;

import static org.apache.karaf.camel.itests.Utils.toKebabCase;
import static org.ops4j.pax.exam.OptionUtils.combine;

public abstract class AbstractCamelKarafITest extends KarafTestSupport {

Expand Down Expand Up @@ -80,16 +80,19 @@ public Option[] config() {
String sshPort = Integer.toString(getAvailablePort(Integer.parseInt(MIN_SSH_PORT), Integer.parseInt(MAX_SSH_PORT)));

Option[] options = new Option[]{
KarafDistributionOption.editConfigurationFilePut("etc/system.properties", "project.version", getVersion()),
KarafDistributionOption.editConfigurationFileExtend("etc/system.properties", "project.target", getBaseDir()),
CoreOptions.systemProperty("project.version").value(getVersion()),
CoreOptions.systemProperty("project.target").value(getBaseDir()),
KarafDistributionOption.features("mvn:org.apache.camel.karaf/apache-camel/"+ getVersion() + "/xml/features", "scr","camel-core"),
CoreOptions.mavenBundle().groupId("org.apache.camel.karaf").artifactId("camel-integration-test").versionAsInProject(),
KarafDistributionOption.editConfigurationFilePut("etc/org.ops4j.pax.web.cfg", "org.osgi.service.http.port", httpPort),
KarafDistributionOption.editConfigurationFilePut("etc/org.apache.karaf.management.cfg", "rmiRegistryPort", rmiRegistryPort),
KarafDistributionOption.editConfigurationFilePut("etc/org.apache.karaf.management.cfg", "rmiServerPort", rmiServerPort),
KarafDistributionOption.editConfigurationFilePut("etc/org.apache.karaf.shell.cfg", "sshPort", sshPort)
};
return Stream.of(super.config(), options).flatMap(Stream::of).toArray(Option[]::new);
Option[] systemProperties = PaxExamWithExternalResource.systemProperties().entrySet().stream()
.map(e -> CoreOptions.systemProperty(e.getKey()).value(e.getValue()))
.toArray(Option[]::new);
return combine(combine(super.config(), options), systemProperties);
}

@Before
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.apache.karaf.camel.itests;

import java.util.Map;

/**
* An interface representing an external resource to set up before a test and guarantee to tear it down afterward.
* Compared to an {@link org.junit.rules.ExternalResource}, the methods {@link #before()} and {@link #after()} are
* executed outside Karaf container, so it can be used to set up external resources like a database, a message broker,
* etc.
* @see TemporaryFile
* @see GenericContainerResource
*/
public interface ExternalResource {

/**
* Sets up the external resource.
*/
void before();

/**
* Tears down the external resource.
*/
void after();

/**
* Gives access to the properties of the external resource like a username, a password or a path, that will be
* provided to the Karaf instance as System properties.
*/
Map<String, String> properties();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.karaf.camel.itests;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.GenericContainer;

/**
* A JUnit ExternalResource that starts and stops a TestContainer.
*
* @param <T> the type of the TestContainer
*/
public class GenericContainerResource<T extends GenericContainer<T>> implements ExternalResource {

private static final Logger LOG = LoggerFactory.getLogger(GenericContainerResource.class);
private final T container;
private final Map<String, String> properties = new HashMap<>();
private final List<ExternalResource> dependencies = new ArrayList<>();
private final Consumer<GenericContainerResource<T>> onStarted;

public GenericContainerResource(T container) {
this(container, t -> {});
}

/**
* Create a GenericContainerResource with the given TestContainer and a callback to be called when the container is
* started.
* @param container the TestContainer
* @param onStarted the callback to be called when the container is started
*/
public GenericContainerResource(T container, Consumer<GenericContainerResource<T>> onStarted) {
this.container = container;
this.onStarted = onStarted;
}

@Override
public void before() {
container.start();
onStarted.accept(this);
for (ExternalResource dependency : dependencies) {
dependency.properties().forEach(this::setProperty);
}
LOG.info("Container {} started", container.getDockerImageName());
}

@Override
public void after() {
container.stop();
for (ExternalResource dependency : dependencies) {
try {
dependency.after();
} catch (Exception e) {
LOG.warn("Error cleaning dependency: {}", dependency.getClass().getName(), e);
}
}
LOG.info("Container {} stopped", container.getDockerImageName());
}

@Override
public Map<String, String> properties() {
return properties;
}

public T getContainer() {
return container;
}

public void setProperty(String key, String value) {
properties.put(key, value);
}

public void addDependency(ExternalResource dependency) {
dependencies.add(dependency);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.karaf.camel.itests;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.junit.runner.Description;
import org.junit.runner.Runner;
import org.junit.runner.manipulation.Filter;
import org.junit.runner.manipulation.Filterable;
import org.junit.runner.manipulation.NoTestsRemainException;
import org.junit.runner.manipulation.Sortable;
import org.junit.runner.manipulation.Sorter;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.ParentRunner;
import org.junit.runners.model.InitializationError;
import org.ops4j.pax.exam.junit.PaxExam;
import org.ops4j.pax.exam.junit.impl.ProbeRunner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* A fork of {@link PaxExam} that supports external resources which can be created and destroyed outside Karaf.
*/
public class PaxExamWithExternalResource extends Runner implements Filterable, Sortable {
private static final Logger LOG = LoggerFactory.getLogger(PaxExamWithExternalResource.class);
private static final ThreadLocal<PaxExamWithExternalResource> current = new ThreadLocal<>();
private final ParentRunner<?> delegate;
private final List<ExternalResource> externalResources;

public PaxExamWithExternalResource(Class<?> testClass) throws InitializationError, InvocationTargetException,
IllegalAccessException {
this.externalResources = beforeAll(testClass);
try {
current.set(this);
this.delegate = new ProbeRunner(testClass);
} finally {
current.remove();
}
}

private List<ExternalResource> beforeAll(Class<?> testClass) throws InvocationTargetException, IllegalAccessException {
UseExternalResourceProvider annotation = testClass.getAnnotation(UseExternalResourceProvider.class);
if (annotation != null) {
List<ExternalResource> result = new ArrayList<>();
for (Method m : annotation.value().getMethods()) {
if (isExternalResourceSupplier(m)) {
ExternalResource externalResource = (ExternalResource) m.invoke(null);
externalResource.before();
result.add(externalResource);
}
}
return result;
}
LOG.warn("Class {} is not annotated with @UseExternalResourceProvider", testClass.getName());
return List.of();
}

private boolean isExternalResourceSupplier(Method m) {
return ExternalResource.class.isAssignableFrom(m.getReturnType()) && m.getParameterTypes().length == 0
&& Modifier.isStatic(m.getModifiers());
}

@Override
public Description getDescription() {
return delegate.getDescription();
}

@Override
public void run(RunNotifier notifier) {
try {
delegate.run(notifier);
} finally {
afterAll();
}
}

private void afterAll() {
for (int i = externalResources.size() - 1; i >= 0; i--) {
try {
externalResources.get(i).after();
} catch (Exception e) {
LOG.warn("Error while cleaning up external resource", e);
}
}
}

@Override
public void filter(Filter filter) throws NoTestsRemainException {
delegate.filter(filter);
}

@Override
public void sort(Sorter sorter) {
delegate.sort(sorter);
}

static Map<String, String> systemProperties() {
PaxExamWithExternalResource value = current.get();
if (value == null) {
return Map.of();
}
return value.externalResources.stream()
.map(ExternalResource::properties)
.map(Map::entrySet)
.flatMap(Set::stream)
.collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue));
}
}
Loading

0 comments on commit 75f4c1d

Please sign in to comment.