Skip to content

Commit

Permalink
Add ability to run tests with different config profiles
Browse files Browse the repository at this point in the history
  • Loading branch information
stuartwdouglas committed Jun 15, 2020
1 parent 4a5c76d commit a2ff59a
Show file tree
Hide file tree
Showing 10 changed files with 320 additions and 7 deletions.
63 changes: 59 additions & 4 deletions docs/src/main/asciidoc/getting-started-testing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,61 @@ public class TestStereotypeTestCase {
}
----

== Testing Different Profiles

So far in all our examples we only start Quarkus once for all tests. Before the first test is run Quarkus will boot,
then all tests will run, then Quarkus will shutdown at the end. This makes for a very fast testing experience however
it is a bit limited as you can't test different configurations.

To get around this Quarkus supports the idea of a test profile. If a test has a different profile to the previously
run test then Quarkus will be shut down and started with the new profile before running the tests. This is obviously
a bit slower, as it adds a shutdown/startup cycle to the test time, but gives a great deal of flexibility.

NOTE: In order to reduce the amount of times Quarkus needs to restart it is recommended that you place all tests
that need a specific profile into their own package, and then run tests alphabetically.

=== Writing a Profile

To implement a test profile we need to implement `io.quarkus.test.junit.QuarkusTestProfile`:

[source,java]
----
package org.acme.getting.started.testing;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import io.quarkus.test.junit.QuarkusTestProfile;
public class MockGreetingProfile implements QuarkusTestProfile {
@Override
public Map<String, String> getConfigOverrides() { <1>
return Collections.singletonMap("quarkus.resteasy.path","/api");
}
@Override
public Set<Class<?>> getEnabledAlternatives() { <2>
return Collections.singleton(MockGreetingService.class);
}
@Override
public String getConfigProfile() { <3>
return "test";
}
}
----
<1> This method allows us to override configuration properties. Here we are changing the JAX-RS root path.
<2> This method allows us to enable CDI `@Alternative` beans. This makes it easy to mock out certain beans functionality.
<3> This can be used to change the config profile. As this default is `test` this does nothing, but is included for completeness.

Now we have defined our profile we need to include it on our test class. We do this with `@TestProfile(MockGreetingProfile.class)`.

All the test profile config is stored in a single class, which makes it easy to tell if the previous test ran with the
same configuration.


== Mock Support

Expand All @@ -311,9 +366,9 @@ mock out a bean for all test classes, or use `QuarkusMock` to mock out beans on

=== CDI `@Alternative` mechanism.

To use this simply override the bean you wish to mock with a class in the `src/test/java` directory, and put the `@Alternative` and `@Priority(1)` annotations on the bean.
To use this simply override the bean you wish to mock with a class in the `src/test/java` directory, and put the `@Alternative` and `@Priority(1)` annotations on the bean.
Alternatively, a convenient `io.quarkus.test.Mock` stereotype annotation could be used.
This built-in stereotype declares `@Alternative`, `@Priority(1)` and `@Dependent`.
This built-in stereotype declares `@Alternative`, `@Priority(1)` and `@Dependent`.
For example if I have the following service:

[source,java]
Expand Down Expand Up @@ -596,7 +651,7 @@ public class SpyGreetingServiceTest {

==== Using `@InjectMock` with `@RestClient`

The `@RegisterRestClient` registers the implementation of the rest-client at runtime, and because the bean needs to be a regular scope, you have to annotate your interface with `@ApplicationScoped`.
The `@RegisterRestClient` registers the implementation of the rest-client at runtime, and because the bean needs to be a regular scope, you have to annotate your interface with `@ApplicationScoped`.

[source,java]
----
Expand All @@ -612,7 +667,7 @@ public interface GreetingService {
}
----

For the test class here is an example:
For the test class here is an example:

[source,java]
----
Expand Down
7 changes: 7 additions & 0 deletions integration-tests/main/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,13 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<runOrder>alphabetical</runOrder>
</configuration>
</plugin>
</plugins>
</build>

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

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import org.jboss.resteasy.annotations.jaxrs.PathParam;

@Path("/greeting")
public class GreetingEndpoint {

@Inject
GreetingService greetingService;

@GET
@Produces(MediaType.TEXT_PLAIN)
@Path("{name}")
public String greet(@PathParam String name) {
return greetingService.greet(name);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.quarkus.it.rest;

import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class GreetingService {
public String greet(String greeting) {
return "Hello " + greeting;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.quarkus.it.main;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Alternative;

import io.quarkus.it.rest.GreetingService;

@ApplicationScoped
@Alternative
public class BonjourService extends GreetingService {

@Override
public String greet(String greeting) {
return "Bonjour " + greeting;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.quarkus.it.main;

import static org.hamcrest.Matchers.is;

import org.junit.jupiter.api.Test;

import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;

@QuarkusTest
public class GreetingNormalTestCase {

@Test
public void included() {
RestAssured.when()
.get("/greeting/Stu")
.then()
.statusCode(200)
.body(is("Hello Stu"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.quarkus.it.main;

import static org.hamcrest.Matchers.is;

import java.util.Collections;
import java.util.Map;
import java.util.Set;

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

import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.QuarkusTestProfile;
import io.quarkus.test.junit.TestProfile;
import io.restassured.RestAssured;

/**
* Tests that QuarkusTestProfile works as expected
*/
@QuarkusTest
@TestProfile(GreetingProfileTestCase.MyProfile.class)
public class GreetingProfileTestCase {

@Test
public void included() {
RestAssured.when()
.get("/greeting/Stu")
.then()
.statusCode(200)
.body(is("Bonjour Stu"));
}

@Test
public void testPortTakesEffect() {
Assertions.assertEquals(7777, RestAssured.port);
}

public static class MyProfile implements QuarkusTestProfile {

@Override
public Map<String, String> getConfigOverrides() {
return Collections.singletonMap("quarkus.http.test-port", "7777");
}

@Override
public Set<Class<?>> getEnabledAlternatives() {
return Collections.singleton(BonjourService.class);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.ServiceLoader;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import javax.enterprise.inject.Alternative;

import org.eclipse.microprofile.config.spi.ConfigProviderResolver;
import org.jboss.jandex.AnnotationInstance;
Expand Down Expand Up @@ -54,6 +58,7 @@
import io.quarkus.deployment.builditem.TestAnnotationBuildItem;
import io.quarkus.deployment.builditem.TestClassBeanBuildItem;
import io.quarkus.deployment.builditem.TestClassPredicateBuildItem;
import io.quarkus.runtime.configuration.ProfileManager;
import io.quarkus.test.common.PathTestHelper;
import io.quarkus.test.common.PropertyTestUtil;
import io.quarkus.test.common.RestAssuredURLManager;
Expand Down Expand Up @@ -90,10 +95,12 @@ public class QuarkusTestExtension
private static List<Object> beforeEachCallbacks = new ArrayList<>();
private static List<Object> afterEachCallbacks = new ArrayList<>();
private static Class<?> quarkusTestMethodContextClass;
private static Class<? extends QuarkusTestProfile> quarkusTestProfile;

private static DeepClone deepClone;

private ExtensionState doJavaStart(ExtensionContext context) throws Throwable {
private ExtensionState doJavaStart(ExtensionContext context, Class<? extends QuarkusTestProfile> profile) throws Throwable {
quarkusTestProfile = profile;
Closeable testResourceManager = null;
try {
final LinkedBlockingDeque<Runnable> shutdownTasks = new LinkedBlockingDeque<>();
Expand All @@ -114,10 +121,38 @@ private ExtensionState doJavaStart(ExtensionContext context) throws Throwable {
}
}
originalCl = Thread.currentThread().getContextClassLoader();
Map<String, String> sysPropRestore = new HashMap<>();
sysPropRestore.put(ProfileManager.QUARKUS_TEST_PROFILE_PROP,
System.getProperty(ProfileManager.QUARKUS_TEST_PROFILE_PROP));

final QuarkusBootstrap.Builder runnerBuilder = QuarkusBootstrap.builder()
.setIsolateDeployment(true)
.setMode(QuarkusBootstrap.Mode.TEST);
if (profile != null) {
QuarkusTestProfile profileInstance = profile.newInstance();
Map<String, String> additional = new HashMap<>(profileInstance.getConfigOverrides());
if (!profileInstance.getEnabledAlternatives().isEmpty()) {
additional.put("quarkus.arc.selected-alternatives", profileInstance.getEnabledAlternatives().stream()
.peek((c) -> {
if (!c.isAnnotationPresent(Alternative.class)) {
throw new RuntimeException(
"Enabled alternative " + c + " is not annotated with @Alternative");
}
})
.map(Class::getName).collect(Collectors.joining(",")));
}
if (profileInstance.getConfigProfile() != null) {
System.setProperty(ProfileManager.QUARKUS_TEST_PROFILE_PROP, profileInstance.getConfigProfile());
}
//we just use system properties for now
//its a lot simpler
for (Map.Entry<String, String> i : additional.entrySet()) {
sysPropRestore.put(i.getKey(), System.getProperty(i.getKey()));
}
for (Map.Entry<String, String> i : additional.entrySet()) {
System.setProperty(i.getKey(), i.getValue());
}
}

runnerBuilder.setProjectRoot(Paths.get("").normalize().toAbsolutePath());

Expand Down Expand Up @@ -176,6 +211,14 @@ public void close() throws IOException {
shutdownTasks.pop().run();
}
} finally {
for (Map.Entry<String, String> entry : sysPropRestore.entrySet()) {
String val = entry.getValue();
if (val == null) {
System.clearProperty(ProfileManager.QUARKUS_TEST_PROFILE_PROP);
} else {
System.setProperty(ProfileManager.QUARKUS_TEST_PROFILE_PROP, val);
}
}
tm.close();
}
}
Expand Down Expand Up @@ -305,10 +348,25 @@ private ExtensionState ensureStarted(ExtensionContext extensionContext) {
ExtensionContext root = extensionContext.getRoot();
ExtensionContext.Store store = root.getStore(ExtensionContext.Namespace.GLOBAL);
ExtensionState state = store.get(ExtensionState.class.getName(), ExtensionState.class);
if (state == null && !failedBoot) {
TestProfile annotation = extensionContext.getRequiredTestClass().getAnnotation(TestProfile.class);
Class<? extends QuarkusTestProfile> selectedProfile = null;
if (annotation != null) {
selectedProfile = annotation.value();
}
boolean wrongProfile = !Objects.equals(selectedProfile, quarkusTestProfile);
if ((state == null && !failedBoot) || wrongProfile) {
if (wrongProfile) {
if (state != null) {
try {
state.close();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
}
PropertyTestUtil.setLogFileProperty();
try {
state = doJavaStart(extensionContext);
state = doJavaStart(extensionContext, selectedProfile);
store.put(ExtensionState.class.getName(), state);

} catch (Throwable e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.quarkus.test.junit;

import java.util.Collections;
import java.util.Map;
import java.util.Set;

/**
* Defines a 'test profile'. Tests run under a test profile
* will have different configuration options to other tests.
*
*/
public interface QuarkusTestProfile {

/**
* Returns additional config to be applied to the test. This
* will override any existing config (including in application.properties),
* however existing config will be merged with this (i.e. application.properties
* config will still take effect, unless a specific config key has been overridden).
*/
default Map<String, String> getConfigOverrides() {
return Collections.emptyMap();
}

/**
* Returns enabled alternatives.
*
* This has the same effect as setting the 'quarkus.arc.selected-alternatives' config key,
* however it may be more convenient.
*/
default Set<Class<?>> getEnabledAlternatives() {
return Collections.emptySet();
}

/**
* Allows the default config profile to be overridden. This basically just sets the quarkus.test.profile system
* property before the test is run.
*
*/
default String getConfigProfile() {
return null;
}
}
Loading

0 comments on commit a2ff59a

Please sign in to comment.