Skip to content

Commit 2bd8d89

Browse files
committed
Integration test support via JUnit Extensions
1 parent 9f37275 commit 2bd8d89

File tree

13 files changed

+473
-22
lines changed

13 files changed

+473
-22
lines changed

TODO

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
* Review what to do with Result class
21
* Review unit test and document it
32
* Review integration test and document it.
43

jooby/src/main/java/io/jooby/AssetHandler.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,7 @@
1818
import javax.annotation.Nonnull;
1919
import java.time.Duration;
2020
import java.time.Instant;
21-
import java.util.Date;
2221
import java.util.List;
23-
import java.util.concurrent.TimeUnit;
2422

2523
/**
2624
* Handler for static resources represented by the {@link Asset} contract.

jooby/src/main/java/io/jooby/Jooby.java

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -610,14 +610,21 @@ public static void runApp(@Nonnull String[] args,
610610
public static void runApp(@Nonnull String[] args, @Nonnull ExecutionMode executionMode,
611611
@Nonnull Class<? extends Jooby> applicationType) {
612612
configurePackage(applicationType);
613-
runApp(args, executionMode, () ->
614-
(Jooby) Stream.of(applicationType.getDeclaredConstructors())
615-
.filter(it -> it.getParameterCount() == 0)
616-
.findFirst()
617-
.map(Throwing.throwingFunction(c -> c.newInstance()))
618-
.orElseThrow(() -> new IllegalArgumentException(
619-
"Default constructor for: " + applicationType.getName()))
620-
);
613+
runApp(args, executionMode, reflectionProvider(applicationType));
614+
}
615+
616+
/**
617+
* Setup default environment, logging (logback or log4j2) and run application.
618+
*
619+
* @param args Application arguments.
620+
* @param executionMode Application execution mode.
621+
* @param applicationType Application type.
622+
* @return Application.
623+
*/
624+
public static Jooby createApp(@Nonnull String[] args, @Nonnull ExecutionMode executionMode,
625+
@Nonnull Class<? extends Jooby> applicationType) {
626+
configurePackage(applicationType);
627+
return createApp(args, executionMode, reflectionProvider(applicationType));
621628
}
622629

623630
/**
@@ -637,7 +644,7 @@ public static void runApp(@Nonnull String[] args, @Nonnull Supplier<Jooby> provi
637644
* @param consumer Application consumer.
638645
*/
639646
public static void runApp(@Nonnull String[] args, @Nonnull Consumer<Jooby> consumer) {
640-
runApp(args, ExecutionMode.DEFAULT, toProvider(consumer));
647+
runApp(args, ExecutionMode.DEFAULT, consumerProvider(consumer));
641648
}
642649

643650
/**
@@ -649,7 +656,7 @@ public static void runApp(@Nonnull String[] args, @Nonnull Consumer<Jooby> consu
649656
*/
650657
public static void runApp(@Nonnull String[] args, @Nonnull ExecutionMode executionMode,
651658
@Nonnull Consumer<Jooby> consumer) {
652-
runApp(args, executionMode, toProvider(consumer));
659+
runApp(args, executionMode, consumerProvider(consumer));
653660
}
654661

655662
/**
@@ -661,6 +668,21 @@ public static void runApp(@Nonnull String[] args, @Nonnull ExecutionMode executi
661668
*/
662669
public static void runApp(@Nonnull String[] args, @Nonnull ExecutionMode executionMode,
663670
@Nonnull Supplier<Jooby> provider) {
671+
Jooby app = createApp(args, executionMode, provider);
672+
Server server = app.start();
673+
server.join();
674+
}
675+
676+
/**
677+
* Setup default environment, logging (logback or log4j2) and run application.
678+
*
679+
* @param args Application arguments.
680+
* @param executionMode Application execution mode.
681+
* @param provider Application provider.
682+
* @return Application.
683+
*/
684+
public static Jooby createApp(@Nonnull String[] args, @Nonnull ExecutionMode executionMode,
685+
@Nonnull Supplier<Jooby> provider) {
664686

665687
configurePackage(provider);
666688

@@ -674,8 +696,7 @@ public static void runApp(@Nonnull String[] args, @Nonnull ExecutionMode executi
674696
if (app.mode == null) {
675697
app.mode = executionMode;
676698
}
677-
Server server = app.start();
678-
server.join();
699+
return app;
679700
}
680701

681702
private static void configurePackage(@Nonnull Object provider) {
@@ -749,7 +770,7 @@ private void fireStop() {
749770
}
750771
}
751772

752-
private static Supplier<Jooby> toProvider(Consumer<Jooby> consumer) {
773+
private static Supplier<Jooby> consumerProvider(Consumer<Jooby> consumer) {
753774
configurePackage(consumer);
754775
return () -> {
755776
Jooby app = new Jooby();
@@ -758,4 +779,14 @@ private static Supplier<Jooby> toProvider(Consumer<Jooby> consumer) {
758779
};
759780
}
760781

782+
private static Supplier<Jooby> reflectionProvider(
783+
@Nonnull Class<? extends Jooby> applicationType) {
784+
return () ->
785+
(Jooby) Stream.of(applicationType.getDeclaredConstructors())
786+
.filter(it -> it.getParameterCount() == 0)
787+
.findFirst()
788+
.map(Throwing.throwingFunction(c -> c.newInstance()))
789+
.orElseThrow(() -> new IllegalArgumentException(
790+
"Default constructor for: " + applicationType.getName()));
791+
}
761792
}

modules/jooby-test/pom.xml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,30 @@
3232
<scope>test</scope>
3333
</dependency>
3434

35+
<dependency>
36+
<groupId>org.junit.jupiter</groupId>
37+
<artifactId>junit-jupiter-api</artifactId>
38+
</dependency>
39+
3540
<dependency>
3641
<groupId>org.jacoco</groupId>
3742
<artifactId>org.jacoco.agent</artifactId>
3843
<classifier>runtime</classifier>
3944
<scope>test</scope>
4045
</dependency>
4146

47+
<dependency>
48+
<groupId>io.jooby</groupId>
49+
<artifactId>jooby-netty</artifactId>
50+
<version>${jooby.version}</version>
51+
<scope>test</scope>
52+
</dependency>
53+
54+
<dependency>
55+
<groupId>com.squareup.okhttp3</groupId>
56+
<artifactId>okhttp</artifactId>
57+
</dependency>
58+
4259
<!-- ASM -->
4360
<dependency>
4461
<groupId>org.ow2.asm</groupId>
@@ -51,5 +68,16 @@
5168
<artifactId>asm-util</artifactId>
5269
<scope>test</scope>
5370
</dependency>
71+
72+
<dependency>
73+
<groupId>commons-io</groupId>
74+
<artifactId>commons-io</artifactId>
75+
<scope>test</scope>
76+
</dependency>
77+
78+
<dependency>
79+
<groupId>ch.qos.logback</groupId>
80+
<artifactId>logback-classic</artifactId>
81+
</dependency>
5482
</dependencies>
5583
</project>
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package io.jooby;
2+
3+
import com.typesafe.config.Config;
4+
import org.junit.jupiter.api.extension.AfterAllCallback;
5+
import org.junit.jupiter.api.extension.AfterEachCallback;
6+
import org.junit.jupiter.api.extension.BeforeAllCallback;
7+
import org.junit.jupiter.api.extension.BeforeEachCallback;
8+
import org.junit.jupiter.api.extension.ExtensionContext;
9+
import org.junit.jupiter.api.extension.ParameterContext;
10+
import org.junit.jupiter.api.extension.ParameterResolutionException;
11+
import org.junit.jupiter.api.extension.ParameterResolver;
12+
import org.junit.jupiter.api.extension.TestInstancePostProcessor;
13+
14+
import java.lang.reflect.Field;
15+
import java.lang.reflect.Method;
16+
import java.lang.reflect.Parameter;
17+
import java.util.Optional;
18+
import java.util.function.Supplier;
19+
20+
/**
21+
* JUnit extension that control lifecycle of Jooby applications on tests. The extension shouldn't
22+
* use it directly. Usage is done via {@link JoobyTest} annotation.
23+
*
24+
* @author edgar
25+
* @since 2.0.0
26+
*/
27+
public class JoobyExtension implements BeforeAllCallback, BeforeEachCallback, AfterAllCallback,
28+
AfterEachCallback, ParameterResolver, TestInstancePostProcessor {
29+
30+
private static final int DEFAULT_PORT = 8911;
31+
32+
static {
33+
System.setProperty("jooby.useShutdownHook", "false");
34+
}
35+
36+
@Override public void beforeAll(ExtensionContext context) throws Exception {
37+
context.getElement().ifPresent(element -> {
38+
JoobyTest metadata = element.getAnnotation(JoobyTest.class);
39+
if (metadata != null) {
40+
startApp(context, metadata.value(), metadata.environment(),
41+
port(metadata.port(), DEFAULT_PORT), metadata.executionMode());
42+
}
43+
});
44+
}
45+
46+
private Jooby startApp(ExtensionContext context, Class applicationClass,
47+
String environment, int port, ExecutionMode executionMode) {
48+
Jooby app = Jooby.createApp(new String[]{environment}, executionMode, applicationClass);
49+
ServerOptions serverOptions = app.getServerOptions();
50+
if (serverOptions == null) {
51+
serverOptions = new ServerOptions();
52+
app.setServerOptions(serverOptions);
53+
}
54+
serverOptions.setPort(port);
55+
Server server = app.start();
56+
ExtensionContext.Store store = getStore(context);
57+
store.put("server", server);
58+
store.put("application", app);
59+
return app;
60+
}
61+
62+
@Override public void beforeEach(ExtensionContext context) throws Exception {
63+
context.getElement().ifPresent(element -> {
64+
JoobyTest metadata = element.getAnnotation(JoobyTest.class);
65+
if (metadata != null) {
66+
startApp(context, metadata.value(), metadata.environment(),
67+
port(metadata.port(), 0), metadata.executionMode());
68+
}
69+
});
70+
}
71+
72+
@Override public void afterAll(ExtensionContext context) throws Exception {
73+
Server server = getStore(context).get("server", Server.class);
74+
if (server != null) {
75+
server.stop();
76+
}
77+
}
78+
79+
@Override public void afterEach(ExtensionContext context) throws Exception {
80+
Server server = getStore(context).get("server", Server.class);
81+
if (server != null) {
82+
server.stop();
83+
}
84+
}
85+
86+
@Override public boolean supportsParameter(ParameterContext parameterContext,
87+
ExtensionContext context) throws ParameterResolutionException {
88+
return joobyParameter(context, parameterContext) != null;
89+
}
90+
91+
@Override public Object resolveParameter(ParameterContext parameterContext,
92+
ExtensionContext context) throws ParameterResolutionException {
93+
return joobyParameter(context, parameterContext).get();
94+
}
95+
96+
private ExtensionContext.Store getStore(ExtensionContext context) {
97+
Optional<Method> testMethod = context.getTestMethod();
98+
ExtensionContext.Namespace namespace = testMethod.isPresent()
99+
? ExtensionContext.Namespace.create(context.getRequiredTestClass(), testMethod.get())
100+
: ExtensionContext.Namespace.create(context.getRequiredTestClass());
101+
return context.getStore(namespace);
102+
}
103+
104+
private Supplier<Object> joobyParameter(ExtensionContext context,
105+
ParameterContext parameterContext) {
106+
Parameter parameter = parameterContext.getParameter();
107+
return injectionPoint(context, parameter.getType(), parameter.getName());
108+
}
109+
110+
private Supplier<Object> injectionPoint(ExtensionContext context,
111+
Class type, String name) {
112+
if (name.equals("serverPath") && type == String.class) {
113+
return () -> {
114+
Jooby app = application(context);
115+
return "http://localhost:" + app.getServerOptions().getPort() + app.getContextPath();
116+
};
117+
}
118+
if (name.equals("serverPort") && type == int.class) {
119+
return () -> {
120+
Jooby app = application(context);
121+
return app.getServerOptions().getPort();
122+
};
123+
}
124+
if (Jooby.class.isAssignableFrom(type)) {
125+
return () -> application(context);
126+
}
127+
if (Environment.class.isAssignableFrom(type)) {
128+
return () -> application(context).getEnvironment();
129+
}
130+
if (Config.class.isAssignableFrom(type)) {
131+
return () -> application(context).getEnvironment().getConfig();
132+
}
133+
throw new IllegalArgumentException("Unsupported: " + type + " " + name);
134+
}
135+
136+
private Jooby application(ExtensionContext context) {
137+
Jooby application = getStore(context).get("application", Jooby.class);
138+
if (application == null) {
139+
ExtensionContext parent = context.getParent()
140+
.orElseThrow(() -> new IllegalStateException("Application not found"));
141+
application = getStore(parent).get("application", Jooby.class);
142+
}
143+
if (application == null) {
144+
throw new IllegalStateException("Application not found");
145+
}
146+
return application;
147+
}
148+
149+
private int port(int port, int fallback) {
150+
return port == -1 ? fallback : port;
151+
}
152+
153+
@Override public void postProcessTestInstance(Object instance, ExtensionContext context)
154+
throws Exception {
155+
for (Field field : instance.getClass().getFields()) {
156+
Supplier<Object> injectionPoint = injectionPoint(context, field.getType(), field.getName());
157+
if (injectionPoint != null) {
158+
field.set(instance, injectionPoint.get());
159+
}
160+
}
161+
}
162+
}

0 commit comments

Comments
 (0)