Skip to content

Commit 2f7b40c

Browse files
Add ability to run tests with different config profiles
1 parent 4a5c76d commit 2f7b40c

File tree

10 files changed

+320
-7
lines changed

10 files changed

+320
-7
lines changed

docs/src/main/asciidoc/getting-started-testing.adoc

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,61 @@ public class TestStereotypeTestCase {
303303
}
304304
----
305305

306+
== Testing Different Profiles
307+
308+
So far in all our examples we only start Quarkus once for all tests. Before the first test is run Quarkus will boot,
309+
then all tests will run, then Quarkus will shutdown at the end. This makes for a very fast testing experience however
310+
it is a bit limited as you can't test different configurations.
311+
312+
To get around this Quarkus supports the idea of a test profile. If a test has a different profile to the previously
313+
run test then Quarkus will be shut down and started with the new profile before running the tests. This is obviously
314+
a bit slower, as it adds a shutdown/startup cycle to the test time, but gives a great deal of flexibility.
315+
316+
NOTE: In order to reduce the amount of times Quarkus needs to restart it is recommended that you place all tests
317+
that need a specific profile into their own package, and then run tests alphabetically.
318+
319+
=== Writing a Profile
320+
321+
To implement a test profile we need to implement `io.quarkus.test.junit.QuarkusTestProfile`:
322+
323+
[source,java]
324+
----
325+
package org.acme.getting.started.testing;
326+
327+
import java.util.Collections;
328+
import java.util.Map;
329+
import java.util.Set;
330+
331+
import io.quarkus.test.junit.QuarkusTestProfile;
332+
333+
public class MockGreetingProfile implements QuarkusTestProfile {
334+
335+
@Override
336+
public Map<String, String> getConfigOverrides() { <1>
337+
return Collections.singletonMap("quarkus.resteasy.path","/api");
338+
}
339+
340+
@Override
341+
public Set<Class<?>> getEnabledAlternatives() { <2>
342+
return Collections.singleton(MockGreetingService.class);
343+
}
344+
345+
346+
@Override
347+
public String getConfigProfile() { <3>
348+
return "test";
349+
}
350+
}
351+
----
352+
<1> This method allows us to override configuration properties. Here we are changing the JAX-RS root path.
353+
<2> This method allows us to enable CDI `@Alternative` beans. This makes it easy to mock out certain beans functionality.
354+
<3> This can be used to change the config profile. As this default is `test` this does nothing, but is included for completeness.
355+
356+
Now we have defined our profile we need to include it on our test class. We do this with `@TestProfile(MockGreetingProfile.class)`.
357+
358+
All the test profile config is stored in a single class, which makes it easy to tell if the previous test ran with the
359+
same configuration.
360+
306361

307362
== Mock Support
308363

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

312367
=== CDI `@Alternative` mechanism.
313368

314-
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.
369+
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.
315370
Alternatively, a convenient `io.quarkus.test.Mock` stereotype annotation could be used.
316-
This built-in stereotype declares `@Alternative`, `@Priority(1)` and `@Dependent`.
371+
This built-in stereotype declares `@Alternative`, `@Priority(1)` and `@Dependent`.
317372
For example if I have the following service:
318373

319374
[source,java]
@@ -596,7 +651,7 @@ public class SpyGreetingServiceTest {
596651

597652
==== Using `@InjectMock` with `@RestClient`
598653

599-
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`.
654+
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`.
600655

601656
[source,java]
602657
----
@@ -612,7 +667,7 @@ public interface GreetingService {
612667
}
613668
----
614669

615-
For the test class here is an example:
670+
For the test class here is an example:
616671

617672
[source,java]
618673
----

integration-tests/main/pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,13 @@
199199
</execution>
200200
</executions>
201201
</plugin>
202+
<plugin>
203+
<groupId>org.apache.maven.plugins</groupId>
204+
<artifactId>maven-surefire-plugin</artifactId>
205+
<configuration>
206+
<runOrder>alphabetical</runOrder>
207+
</configuration>
208+
</plugin>
202209
</plugins>
203210
</build>
204211

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package io.quarkus.it.rest;
2+
3+
import javax.inject.Inject;
4+
import javax.ws.rs.GET;
5+
import javax.ws.rs.Path;
6+
import javax.ws.rs.Produces;
7+
import javax.ws.rs.core.MediaType;
8+
9+
import org.jboss.resteasy.annotations.jaxrs.PathParam;
10+
11+
@Path("/greeting")
12+
public class GreetingEndpoint {
13+
14+
@Inject
15+
GreetingService greetingService;
16+
17+
@GET
18+
@Produces(MediaType.TEXT_PLAIN)
19+
@Path("{name}")
20+
public String greet(@PathParam String name) {
21+
return greetingService.greet(name);
22+
}
23+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package io.quarkus.it.rest;
2+
3+
import javax.enterprise.context.ApplicationScoped;
4+
5+
@ApplicationScoped
6+
public class GreetingService {
7+
public String greet(String greeting) {
8+
return "Hello " + greeting;
9+
}
10+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.quarkus.it.main;
2+
3+
import javax.enterprise.context.ApplicationScoped;
4+
import javax.enterprise.inject.Alternative;
5+
6+
import io.quarkus.it.rest.GreetingService;
7+
8+
@ApplicationScoped
9+
@Alternative
10+
public class BonjourService extends GreetingService {
11+
12+
@Override
13+
public String greet(String greeting) {
14+
return "Bonjour " + greeting;
15+
}
16+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package io.quarkus.it.main;
2+
3+
import static org.hamcrest.Matchers.is;
4+
5+
import org.junit.jupiter.api.Test;
6+
7+
import io.quarkus.test.junit.QuarkusTest;
8+
import io.restassured.RestAssured;
9+
10+
@QuarkusTest
11+
public class GreetingNormalTestCase {
12+
13+
@Test
14+
public void included() {
15+
RestAssured.when()
16+
.get("/greeting/Stu")
17+
.then()
18+
.statusCode(200)
19+
.body(is("Hello Stu"));
20+
}
21+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package io.quarkus.it.main;
2+
3+
import static org.hamcrest.Matchers.is;
4+
5+
import java.util.Collections;
6+
import java.util.Map;
7+
import java.util.Set;
8+
9+
import org.junit.jupiter.api.Assertions;
10+
import org.junit.jupiter.api.Test;
11+
12+
import io.quarkus.test.junit.QuarkusTest;
13+
import io.quarkus.test.junit.QuarkusTestProfile;
14+
import io.quarkus.test.junit.TestProfile;
15+
import io.restassured.RestAssured;
16+
17+
/**
18+
* Tests that QuarkusTestProfile works as expected
19+
*/
20+
@QuarkusTest
21+
@TestProfile(GreetingProfileTestCase.MyProfile.class)
22+
public class GreetingProfileTestCase {
23+
24+
@Test
25+
public void included() {
26+
RestAssured.when()
27+
.get("/greeting/Stu")
28+
.then()
29+
.statusCode(200)
30+
.body(is("Bonjour Stu"));
31+
}
32+
33+
@Test
34+
public void testPortTakesEffect() {
35+
Assertions.assertEquals(7777, RestAssured.port);
36+
}
37+
38+
public static class MyProfile implements QuarkusTestProfile {
39+
40+
@Override
41+
public Map<String, String> getConfigOverrides() {
42+
return Collections.singletonMap("quarkus.http.test-port", "7777");
43+
}
44+
45+
@Override
46+
public Set<Class<?>> getEnabledAlternatives() {
47+
return Collections.singleton(BonjourService.class);
48+
}
49+
}
50+
}

test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,15 @@
1515
import java.util.HashMap;
1616
import java.util.List;
1717
import java.util.Map;
18+
import java.util.Objects;
1819
import java.util.ServiceLoader;
1920
import java.util.concurrent.LinkedBlockingDeque;
2021
import java.util.function.Consumer;
2122
import java.util.function.Function;
2223
import java.util.function.Predicate;
24+
import java.util.stream.Collectors;
25+
26+
import javax.enterprise.inject.Alternative;
2327

2428
import org.eclipse.microprofile.config.spi.ConfigProviderResolver;
2529
import org.jboss.jandex.AnnotationInstance;
@@ -54,6 +58,7 @@
5458
import io.quarkus.deployment.builditem.TestAnnotationBuildItem;
5559
import io.quarkus.deployment.builditem.TestClassBeanBuildItem;
5660
import io.quarkus.deployment.builditem.TestClassPredicateBuildItem;
61+
import io.quarkus.runtime.configuration.ProfileManager;
5762
import io.quarkus.test.common.PathTestHelper;
5863
import io.quarkus.test.common.PropertyTestUtil;
5964
import io.quarkus.test.common.RestAssuredURLManager;
@@ -90,10 +95,12 @@ public class QuarkusTestExtension
9095
private static List<Object> beforeEachCallbacks = new ArrayList<>();
9196
private static List<Object> afterEachCallbacks = new ArrayList<>();
9297
private static Class<?> quarkusTestMethodContextClass;
98+
private static Class<? extends QuarkusTestProfile> quarkusTestProfile;
9399

94100
private static DeepClone deepClone;
95101

96-
private ExtensionState doJavaStart(ExtensionContext context) throws Throwable {
102+
private ExtensionState doJavaStart(ExtensionContext context, Class<? extends QuarkusTestProfile> profile) throws Throwable {
103+
quarkusTestProfile = profile;
97104
Closeable testResourceManager = null;
98105
try {
99106
final LinkedBlockingDeque<Runnable> shutdownTasks = new LinkedBlockingDeque<>();
@@ -114,10 +121,38 @@ private ExtensionState doJavaStart(ExtensionContext context) throws Throwable {
114121
}
115122
}
116123
originalCl = Thread.currentThread().getContextClassLoader();
124+
Map<String, String> sysPropRestore = new HashMap<>();
125+
sysPropRestore.put(ProfileManager.QUARKUS_TEST_PROFILE_PROP,
126+
System.getProperty(ProfileManager.QUARKUS_TEST_PROFILE_PROP));
117127

118128
final QuarkusBootstrap.Builder runnerBuilder = QuarkusBootstrap.builder()
119129
.setIsolateDeployment(true)
120130
.setMode(QuarkusBootstrap.Mode.TEST);
131+
if (profile != null) {
132+
QuarkusTestProfile profileInstance = profile.newInstance();
133+
Map<String, String> additional = new HashMap<>(profileInstance.getConfigOverrides());
134+
if (!profileInstance.getEnabledAlternatives().isEmpty()) {
135+
additional.put("quarkus.arc.selected-alternatives", profileInstance.getEnabledAlternatives().stream()
136+
.peek((c) -> {
137+
if (!c.isAnnotationPresent(Alternative.class)) {
138+
throw new RuntimeException(
139+
"Enabled alternative " + c + " is not annotated with @Alternative");
140+
}
141+
})
142+
.map(Class::getName).collect(Collectors.joining(",")));
143+
}
144+
if (profileInstance.getConfigProfile() != null) {
145+
System.setProperty(ProfileManager.QUARKUS_TEST_PROFILE_PROP, profileInstance.getConfigProfile());
146+
}
147+
//we just use system properties for now
148+
//its a lot simpler
149+
for (Map.Entry<String, String> i : additional.entrySet()) {
150+
sysPropRestore.put(i.getKey(), System.getProperty(i.getKey()));
151+
}
152+
for (Map.Entry<String, String> i : additional.entrySet()) {
153+
System.setProperty(i.getKey(), i.getValue());
154+
}
155+
}
121156

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

@@ -176,6 +211,14 @@ public void close() throws IOException {
176211
shutdownTasks.pop().run();
177212
}
178213
} finally {
214+
for (Map.Entry<String, String> entry : sysPropRestore.entrySet()) {
215+
String val = entry.getValue();
216+
if (val == null) {
217+
System.clearProperty(entry.getKey());
218+
} else {
219+
System.setProperty(entry.getKey(), val);
220+
}
221+
}
179222
tm.close();
180223
}
181224
}
@@ -305,10 +348,25 @@ private ExtensionState ensureStarted(ExtensionContext extensionContext) {
305348
ExtensionContext root = extensionContext.getRoot();
306349
ExtensionContext.Store store = root.getStore(ExtensionContext.Namespace.GLOBAL);
307350
ExtensionState state = store.get(ExtensionState.class.getName(), ExtensionState.class);
308-
if (state == null && !failedBoot) {
351+
TestProfile annotation = extensionContext.getRequiredTestClass().getAnnotation(TestProfile.class);
352+
Class<? extends QuarkusTestProfile> selectedProfile = null;
353+
if (annotation != null) {
354+
selectedProfile = annotation.value();
355+
}
356+
boolean wrongProfile = !Objects.equals(selectedProfile, quarkusTestProfile);
357+
if ((state == null && !failedBoot) || wrongProfile) {
358+
if (wrongProfile) {
359+
if (state != null) {
360+
try {
361+
state.close();
362+
} catch (Throwable throwable) {
363+
throwable.printStackTrace();
364+
}
365+
}
366+
}
309367
PropertyTestUtil.setLogFileProperty();
310368
try {
311-
state = doJavaStart(extensionContext);
369+
state = doJavaStart(extensionContext, selectedProfile);
312370
store.put(ExtensionState.class.getName(), state);
313371

314372
} catch (Throwable e) {
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package io.quarkus.test.junit;
2+
3+
import java.util.Collections;
4+
import java.util.Map;
5+
import java.util.Set;
6+
7+
/**
8+
* Defines a 'test profile'. Tests run under a test profile
9+
* will have different configuration options to other tests.
10+
*
11+
*/
12+
public interface QuarkusTestProfile {
13+
14+
/**
15+
* Returns additional config to be applied to the test. This
16+
* will override any existing config (including in application.properties),
17+
* however existing config will be merged with this (i.e. application.properties
18+
* config will still take effect, unless a specific config key has been overridden).
19+
*/
20+
default Map<String, String> getConfigOverrides() {
21+
return Collections.emptyMap();
22+
}
23+
24+
/**
25+
* Returns enabled alternatives.
26+
*
27+
* This has the same effect as setting the 'quarkus.arc.selected-alternatives' config key,
28+
* however it may be more convenient.
29+
*/
30+
default Set<Class<?>> getEnabledAlternatives() {
31+
return Collections.emptySet();
32+
}
33+
34+
/**
35+
* Allows the default config profile to be overridden. This basically just sets the quarkus.test.profile system
36+
* property before the test is run.
37+
*
38+
*/
39+
default String getConfigProfile() {
40+
return null;
41+
}
42+
}

0 commit comments

Comments
 (0)