Skip to content

Commit

Permalink
TestResourceManager: support test resource annotations on profile
Browse files Browse the repository at this point in the history
  • Loading branch information
FroMage committed Feb 17, 2021
1 parent d1ca807 commit 85e005f
Show file tree
Hide file tree
Showing 13 changed files with 109 additions and 26 deletions.
6 changes: 3 additions & 3 deletions docs/src/main/asciidoc/getting-started-testing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -987,8 +987,8 @@ A very common need is to start some services on which your Quarkus application d
By simply annotating any test in the test suite with `@QuarkusTestResource`, Quarkus will run the corresponding `QuarkusTestResourceLifecycleManager` before any tests are run.
A test suite is also free to utilize multiple `@QuarkusTestResource` annotations, in which case all the corresponding `QuarkusTestResourceLifecycleManager` objects will be run before the tests. When using multiple test resources they can be started concurrently. For that you need to set `@QuarkusTestResource(parallel = true)`.

NOTE: test resources are global, even if they are defined on a test class, which means they will all be activated for all tests, even though we do
remove duplicates. If you want to only enable a test resource on a single test class, you can use `@QuarkusTestResource(restrictToAnnotatedTest = true)`.
NOTE: test resources are global, even if they are defined on a test class or custom profile, which means they will all be activated for all tests, even though we do
remove duplicates. If you want to only enable a test resource on a single test class or test profile, you can use `@QuarkusTestResource(restrictToAnnotatedClass = true)`.

Quarkus provides a few implementations of `QuarkusTestResourceLifecycleManager` out of the box (see `io.quarkus.test.h2.H2DatabaseTestResource` which starts an H2 database, or `io.quarkus.test.kubernetes.client.KubernetesMockServerTestResource` which starts a mock Kubernetes API server),
but it is common to create custom implementations to address specific application needs.
Expand All @@ -1001,7 +1001,7 @@ It is possible to write test resources that are enabled and configured using ann
on an annotation which will be used to enable and configure the test resource.

For example, this defines the `@WithKubernetesTestServer` annotation, which you can use on your tests to activate the `KubernetesServerTestResource`,
but only for the annotated test class.
but only for the annotated test class. You can also place them on your `QuarkusTestProfile` test profiles.

[source,java]
----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import io.quarkus.test.QuarkusProdModeTest;
import io.quarkus.test.common.QuarkusTestResource;

@QuarkusTestResource(value = CustomKubernetesMockServerTestResource.class, restrictToAnnotatedTest = true)
@QuarkusTestResource(value = CustomKubernetesMockServerTestResource.class, restrictToAnnotatedClass = true)
public class AbsentConfigMapPropertiesPMT {

@RegisterExtension
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;

@QuarkusTestResource(value = CustomKubernetesMockServerTestResource.class, restrictToAnnotatedTest = true)
@QuarkusTestResource(value = CustomKubernetesMockServerTestResource.class, restrictToAnnotatedClass = true)
@QuarkusTest
public class ConfigMapPropertiesTest {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
* KubernetesClientTest.TestResource contains the entire process of setting up the Mock Kubernetes API Server
* It has to live there otherwise the Kubernetes client in native mode won't be able to locate the mock API Server
*/
@QuarkusTestResource(value = CustomKubernetesMockServerTestResource.class, restrictToAnnotatedTest = true)
@QuarkusTestResource(value = CustomKubernetesMockServerTestResource.class, restrictToAnnotatedClass = true)
@QuarkusTest
public class KubernetesClientTest {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* KubernetesClientTest.TestResource contains the entire process of setting up the Mock Kubernetes API Server
* It has to live there otherwise the Kubernetes client in native mode won't be able to locate the mock API Server
*/
@QuarkusTestResource(value = CustomKubernetesTestServerTestResource.class, restrictToAnnotatedTest = true)
@QuarkusTestResource(value = CustomKubernetesTestServerTestResource.class, restrictToAnnotatedClass = true)
@QuarkusTest
public class KubernetesNewClientTest {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package io.quarkus.it.kubernetes.client;

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

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import io.fabric8.kubernetes.client.server.mock.KubernetesServer;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.QuarkusTestProfile;
import io.quarkus.test.junit.TestProfile;
import io.quarkus.test.kubernetes.client.KubernetesTestServer;
import io.quarkus.test.kubernetes.client.WithKubernetesTestServer;

/*
* This class has no native-image test because it relies on setting config overrides that clash
* with native image build config.
* This is the same test as KubernetesTestServerTest but with the test resource annotation on the profile
*/
@TestProfile(KubernetesTestServerOnProfileTest.MyProfile.class)
@QuarkusTest
public class KubernetesTestServerOnProfileTest {

private static KubernetesServer setupServer;

public static class Setup implements Consumer<KubernetesServer> {

@Override
public void accept(KubernetesServer t) {
setupServer = t;
}

}

@KubernetesTestServer
private KubernetesServer mockServer;

@Test
public void testConfiguration() throws InterruptedException {
// we can't really test CRUD, and HTTPS doesn't work
Assertions.assertEquals(10001, mockServer.getMockServer().getPort());
Assertions.assertSame(mockServer, setupServer);
}

@WithKubernetesTestServer(https = false, crud = true, port = 10001, setup = KubernetesTestServerOnProfileTest.Setup.class)
public static class MyProfile implements QuarkusTestProfile {

@Override
public Map<String, String> getConfigOverrides() {
Map<String, String> overrides = new HashMap<>();
// do not fetch config from kubernetes
overrides.put("quarkus.kubernetes-config.enabled", "false");
overrides.put("quarkus.kubernetes-config.secrets.enabled", "false");
// get rid of errors due to us not populating config from kubernetes
overrides.put("dummy", "asd");
overrides.put("some.prop1", "asd");
overrides.put("some.prop2", "asd");
overrides.put("some.prop3", "asd");
overrides.put("some.prop4", "asd");
overrides.put("some.prop5", "asd");
overrides.put("secret.prop1", "asd");
overrides.put("secret.prop2", "asd");
overrides.put("secret.prop3", "asd");
overrides.put("secret.prop4", "asd");
overrides.put("overridden.secret", "asd");
overrides.put("dummysecret", "asd");
return overrides;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import io.quarkus.test.junit.QuarkusTestProfile;
import io.quarkus.test.junit.TestProfile;

@QuarkusTestResource(value = CustomKubernetesMockServerTestResource.class, restrictToAnnotatedTest = true)
@QuarkusTestResource(value = CustomKubernetesMockServerTestResource.class, restrictToAnnotatedClass = true)
@TestProfile(NamespacedConfigMapPropertiesTest.MyProfile.class)
@QuarkusTest
public class NamespacedConfigMapPropertiesTest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;

@QuarkusTestResource(value = CustomKubernetesMockServerTestResource.class, restrictToAnnotatedTest = true)
@QuarkusTestResource(value = CustomKubernetesMockServerTestResource.class, restrictToAnnotatedClass = true)
@QuarkusTest
public class SecretPropertiesTest {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@
boolean parallel() default false;

/**
* Whether this annotation should only be enabled if it is placed on the currently running test class.
* Whether this annotation should only be enabled if it is placed on the currently running test class or test profile.
* Note that this defaults to true for meta-annotations since meta-annotations are only considered
* for the current test class.
* for the current test class or test profile.
*/
boolean restrictToAnnotatedTest() default false;
boolean restrictToAnnotatedClass() default false;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@

public class TestResourceManager implements Closeable {

private static final DotName DOTNAME_QUARKUS_TEST_RESOURCE = DotName.createSimple(QuarkusTestResource.class.getName());

private final List<TestResourceEntry> sequentialTestResourceEntries;
private final List<TestResourceEntry> parallelTestResourceEntries;
private final List<TestResourceEntry> allTestResourceEntries;
Expand All @@ -40,10 +38,10 @@ public class TestResourceManager implements Closeable {
private boolean hasPerTestResources = false;

public TestResourceManager(Class<?> testClass) {
this(testClass, Collections.emptyList(), false);
this(testClass, null, Collections.emptyList(), false);
}

public TestResourceManager(Class<?> testClass, List<TestResourceClassEntry> additionalTestResources,
public TestResourceManager(Class<?> testClass, Class<?> profileClass, List<TestResourceClassEntry> additionalTestResources,
boolean disableGlobalTestResources) {
this.parallelTestResourceEntries = new ArrayList<>();
this.sequentialTestResourceEntries = new ArrayList<>();
Expand All @@ -54,7 +52,7 @@ public TestResourceManager(Class<?> testClass, List<TestResourceClassEntry> addi
if (disableGlobalTestResources) {
uniqueEntries = new HashSet<>(additionalTestResources);
} else {
uniqueEntries = getUniqueTestResourceClassEntries(testClass, additionalTestResources);
uniqueEntries = getUniqueTestResourceClassEntries(testClass, profileClass, additionalTestResources);
}
Set<TestResourceClassEntry> remainingUniqueEntries = initParallelTestResources(uniqueEntries);
initSequentialTestResources(remainingUniqueEntries);
Expand Down Expand Up @@ -250,20 +248,30 @@ private TestResourceManager.TestResourceEntry buildTestResourceEntry(TestResourc
}
}

private Set<TestResourceClassEntry> getUniqueTestResourceClassEntries(Class<?> testClass,
private Set<TestResourceClassEntry> getUniqueTestResourceClassEntries(Class<?> testClass, Class<?> profileClass,
List<TestResourceClassEntry> additionalTestResources) {
IndexView index = TestClassIndexer.readIndex(testClass);
Set<TestResourceClassEntry> uniqueEntries = new HashSet<>();
// reload the test class in the right CL
// reload the test and profile classes in the right CL
Class<?> testClassFromTCCL;
Class<?> profileClassFromTCCL;
try {
testClassFromTCCL = Class.forName(testClass.getName(), false, Thread.currentThread().getContextClassLoader());
if (profileClass != null) {
profileClassFromTCCL = Class.forName(profileClass.getName(), false,
Thread.currentThread().getContextClassLoader());
} else {
profileClassFromTCCL = null;
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
// handle meta-annotations: in this case we must rely on reflection because meta-annotations are not indexed
// because they are not in the user's test folder but come from test extensions
collectMetaAnnotations(testClassFromTCCL, uniqueEntries);
if (profileClassFromTCCL != null) {
collectMetaAnnotations(profileClassFromTCCL, uniqueEntries);
}
for (AnnotationInstance annotation : findQuarkusTestResourceInstances(testClass, index)) {
try {
Class<? extends QuarkusTestResourceLifecycleManager> testResourceClass = loadTestResourceClassFromTCCL(
Expand All @@ -287,7 +295,7 @@ private Set<TestResourceClassEntry> getUniqueTestResourceClassEntries(Class<?> t
isParallel = parallelAnnotationValue.asBoolean();
}

AnnotationValue restrict = annotation.value("restrictToAnnotatedTest");
AnnotationValue restrict = annotation.value("restrictToAnnotatedClass");
if (restrict != null && restrict.asBoolean()) {
hasPerTestResources = true;
}
Expand All @@ -309,7 +317,7 @@ private void collectMetaAnnotations(Class<?> testClassFromTCCL, Set<TestResource
if (annotationAnnotation.annotationType() == QuarkusTestResource.class) {
QuarkusTestResource testResource = (QuarkusTestResource) annotationAnnotation;

// NOTE: we don't need to check restrictToAnnotatedTest because by design config-based annotations
// NOTE: we don't need to check restrictToAnnotatedClass because by design config-based annotations
// are not discovered outside the test class, so they're restricted
Class<? extends QuarkusTestResourceLifecycleManager> testResourceClass = testResource.value();

Expand Down Expand Up @@ -379,7 +387,7 @@ public boolean hasPerTestResources() {
}

private boolean keepTestResourceAnnotation(AnnotationInstance annotation, ClassInfo targetClass, Set<String> testClasses) {
AnnotationValue restrict = annotation.value("restrictToAnnotatedTest");
AnnotationValue restrict = annotation.value("restrictToAnnotatedClass");
if (restrict != null && restrict.asBoolean()) {
return testClasses.contains(targetClass.name().toString('.'));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.io.Closeable;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -173,7 +174,8 @@ private ExtensionState doNativeStart(ExtensionContext context, Class<? extends Q
}
}

testResourceManager = new TestResourceManager(requiredTestClass);
testResourceManager = new TestResourceManager(requiredTestClass, quarkusTestProfile,
Collections.emptyList(), profileInstance.disableGlobalTestResources());
testResourceManager.init();
hasPerTestResources = testResourceManager.hasPerTestResources();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,8 +313,9 @@ private ExtensionState doJavaStart(ExtensionContext context, Class<? extends Qua

//must be done after the TCCL has been set
testResourceManager = (Closeable) startupAction.getClassLoader().loadClass(TestResourceManager.class.getName())
.getConstructor(Class.class, List.class, boolean.class)
.getConstructor(Class.class, Class.class, List.class, boolean.class)
.newInstance(requiredTestClass,
profile != null ? profile : null,
getAdditionalTestResources(profileInstance, startupAction.getClassLoader()),
profileInstance != null && profileInstance.disableGlobalTestResources());
testResourceManager.getClass().getMethod("init").invoke(testResourceManager);
Expand Down Expand Up @@ -1192,7 +1193,7 @@ static boolean hasPerTestResources(ExtensionContext extensionContext) {
public static boolean hasPerTestResources(Class<?> requiredTestClass) {
while (requiredTestClass != Object.class) {
for (QuarkusTestResource testResource : requiredTestClass.getAnnotationsByType(QuarkusTestResource.class)) {
if (testResource.restrictToAnnotatedTest()) {
if (testResource.restrictToAnnotatedClass()) {
return true;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* Use on your test resource to get a mock {@link KubernetesServer} spawn up, and injectable with {@link KubernetesTestServer}.
* This annotation is only active when used on a test class, and only for this test class.
*/
@QuarkusTestResource(value = KubernetesServerTestResource.class, restrictToAnnotatedTest = true)
@QuarkusTestResource(value = KubernetesServerTestResource.class, restrictToAnnotatedClass = true)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface WithKubernetesTestServer {
Expand Down

0 comments on commit 85e005f

Please sign in to comment.