Skip to content

Commit 59d752e

Browse files
committed
#203 #204 - Add @InjectTest + TestScopeBean with helper methods to programmatically use the test scope
1 parent 5987d59 commit 59d752e

File tree

12 files changed

+242
-62
lines changed

12 files changed

+242
-62
lines changed

blackbox-test-inject/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<parent>
66
<artifactId>avaje-inject-parent</artifactId>
77
<groupId>io.avaje</groupId>
8-
<version>8.2</version>
8+
<version>8.3</version>
99
</parent>
1010
<modelVersion>4.0.0</modelVersion>
1111

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.example.myapp;
2+
3+
import io.avaje.inject.test.InjectTest;
4+
import jakarta.inject.Inject;
5+
import org.junit.jupiter.api.Test;
6+
7+
import static org.junit.jupiter.api.Assertions.assertEquals;
8+
9+
@InjectTest
10+
class InjectExtension_viaAnnotation_Test {
11+
12+
@Inject
13+
HelloService helloService;
14+
15+
@Test
16+
void hello_1() {
17+
assertEquals("hello+TestHelloData", helloService.hello());
18+
}
19+
20+
@Test
21+
void hello_2() {
22+
assertEquals("hello+TestHelloData", helloService.hello());
23+
}
24+
25+
}

inject-generator/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<parent>
55
<groupId>io.avaje</groupId>
66
<artifactId>avaje-inject-parent</artifactId>
7-
<version>8.2</version>
7+
<version>8.3</version>
88
</parent>
99

1010
<artifactId>avaje-inject-generator</artifactId>
@@ -16,7 +16,7 @@
1616
<dependency>
1717
<groupId>io.avaje</groupId>
1818
<artifactId>avaje-inject</artifactId>
19-
<version>8.2</version>
19+
<version>8.3</version>
2020
</dependency>
2121

2222
<!-- test dependencies -->

inject-test/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<parent>
55
<groupId>io.avaje</groupId>
66
<artifactId>avaje-inject-parent</artifactId>
7-
<version>8.2</version>
7+
<version>8.3</version>
88
</parent>
99

1010
<artifactId>avaje-inject-test</artifactId>
@@ -124,7 +124,7 @@
124124
<path>
125125
<groupId>io.avaje</groupId>
126126
<artifactId>avaje-inject-generator</artifactId>
127-
<version>8.2</version>
127+
<version>8.3</version>
128128
</path>
129129
</annotationProcessorPaths>
130130
</configuration>

inject-test/src/main/java/io/avaje/inject/test/InjectExtension.java

Lines changed: 7 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,17 @@
22

33
import io.avaje.inject.BeanScope;
44
import io.avaje.inject.BeanScopeBuilder;
5-
import io.avaje.inject.spi.Module;
65
import org.junit.jupiter.api.extension.AfterEachCallback;
76
import org.junit.jupiter.api.extension.BeforeAllCallback;
87
import org.junit.jupiter.api.extension.BeforeEachCallback;
98
import org.junit.jupiter.api.extension.ExtensionContext;
109
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
1110

12-
import java.io.*;
1311
import java.lang.System.Logger.Level;
14-
import java.net.URL;
15-
import java.util.*;
12+
import java.util.ArrayList;
13+
import java.util.List;
1614
import java.util.concurrent.locks.ReentrantLock;
1715

18-
import static java.util.Collections.singletonList;
19-
2016
/**
2117
* Junit 5 extension for avaje inject.
2218
* <p>
@@ -58,54 +54,11 @@ public void close() {
5854
}
5955

6056
private void initialiseGlobalTestScope(ExtensionContext context) {
61-
List<TestModule> testModules = new ArrayList<>();
62-
for (TestModule next : ServiceLoader.load(TestModule.class)) {
63-
testModules.add(next);
64-
}
65-
if (testModules.isEmpty()) {
66-
registerViaResources(context);
67-
} else {
68-
registerTestModule(context, testModules);
69-
}
70-
}
71-
72-
private void registerTestModule(ExtensionContext context, List<TestModule> testModules) {
73-
log.log(Level.DEBUG, "Building global test BeanScope (as parent scope for tests)");
74-
globalTestScope = BeanScope.newBuilder()
75-
.withModules(testModules.toArray(Module[]::new))
76-
.build();
77-
78-
log.log(Level.TRACE, "register global test BeanScope with beans %s", globalTestScope);
79-
context.getRoot().getStore(Namespace.GLOBAL).put(InjectExtension.class.getCanonicalName(), this);
80-
}
81-
82-
/**
83-
* Fallback when ServiceLoader does not work in module-path for generated test service.
84-
*/
85-
private void registerViaResources(ExtensionContext context) {
86-
try {
87-
URL url = ClassLoader.getSystemResource("META-INF/services/io.avaje.inject.test.TestModule");
88-
String className = readServiceClassName(url);
89-
if (className != null) {
90-
Class<?> cls = Class.forName(className);
91-
TestModule testModule = (TestModule) cls.getDeclaredConstructor().newInstance();
92-
registerTestModule(context, singletonList(testModule));
93-
}
94-
} catch (Throwable e) {
95-
throw new RuntimeException("Error trying to create TestModule", e);
96-
}
97-
}
98-
99-
private String readServiceClassName(URL url) throws IOException {
100-
if (url != null) {
101-
InputStream is = url.openStream();
102-
if (is != null) {
103-
try (LineNumberReader lineNumberReader = new LineNumberReader(new InputStreamReader(is))) {
104-
return lineNumberReader.readLine();
105-
}
106-
}
57+
globalTestScope = TestBeanScope.init(false);
58+
if (globalTestScope != null) {
59+
log.log(Level.TRACE, "register global test BeanScope with beans {0}", globalTestScope);
60+
context.getRoot().getStore(Namespace.GLOBAL).put(InjectExtension.class.getCanonicalName(), this);
10761
}
108-
return null;
10962
}
11063

11164
/**
@@ -114,7 +67,6 @@ private String readServiceClassName(URL url) throws IOException {
11467
@Override
11568
public void beforeEach(final ExtensionContext context) {
11669
final List<MetaReader> readers = createMetaReaders(context);
117-
11870
final BeanScopeBuilder builder = BeanScope.newBuilder();
11971
if (globalTestScope != null) {
12072
builder.withParent(globalTestScope, false);
@@ -129,7 +81,7 @@ public void beforeEach(final ExtensionContext context) {
12981
for (MetaReader reader : readers) {
13082
reader.setFromScope(beanScope);
13183
}
132-
log.log(Level.TRACE, "test setup with %s", readers);
84+
log.log(Level.TRACE, "test setup with {0}", readers);
13385
context.getStore(INJECT_NS).put(BEAN_SCOPE, beanScope);
13486
}
13587

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package io.avaje.inject.test;
2+
3+
import org.junit.jupiter.api.extension.ExtendWith;
4+
5+
import java.lang.annotation.ElementType;
6+
import java.lang.annotation.Retention;
7+
import java.lang.annotation.RetentionPolicy;
8+
import java.lang.annotation.Target;
9+
10+
/**
11+
* An avaje-inject test supporting {@code @Inject} along with Mockito
12+
* annotations - {@code @Mock, @Spy, @Captor}.
13+
* <p>
14+
* This is a JUnit 5 extension.
15+
*/
16+
@ExtendWith(InjectExtension.class)
17+
@Target(ElementType.TYPE)
18+
@Retention(RetentionPolicy.RUNTIME)
19+
public @interface InjectTest {
20+
21+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package io.avaje.inject.test;
2+
3+
import io.avaje.inject.BeanScope;
4+
import io.avaje.inject.spi.Module;
5+
import io.avaje.lang.Nullable;
6+
7+
import java.io.IOException;
8+
import java.io.InputStream;
9+
import java.io.InputStreamReader;
10+
import java.io.LineNumberReader;
11+
import java.net.URL;
12+
import java.util.ArrayList;
13+
import java.util.Enumeration;
14+
import java.util.List;
15+
import java.util.ServiceLoader;
16+
import java.util.concurrent.locks.ReentrantLock;
17+
18+
/**
19+
* Internal helper to build the test scope BeanScope.
20+
* <p>
21+
* Takes into when Service loading does not work such as when using module-path.
22+
* In that case loads the META-INF services resources and uses reflection.
23+
*/
24+
final class TSBuild {
25+
26+
private static final ReentrantLock lock = new ReentrantLock();
27+
private static BeanScope SCOPE;
28+
29+
private final boolean shutdownHook;
30+
31+
/**
32+
* Create and return the test BeanScope. A BeanScope is created each
33+
* time this method is called.
34+
*/
35+
@Nullable
36+
static BeanScope create(boolean shutdownHook) {
37+
return new TSBuild(shutdownHook).build();
38+
}
39+
40+
/**
41+
* Return the test BeanScope only creating once.
42+
*/
43+
@Nullable
44+
static BeanScope initialise(boolean shutdownHook) {
45+
lock.lock();
46+
try {
47+
if (SCOPE == null) {
48+
SCOPE = create(shutdownHook);
49+
}
50+
return SCOPE;
51+
} finally {
52+
lock.unlock();
53+
}
54+
}
55+
56+
TSBuild(boolean shutdownHook) {
57+
this.shutdownHook = shutdownHook;
58+
}
59+
60+
@Nullable
61+
private BeanScope build() {
62+
List<TestModule> testModules = new ArrayList<>();
63+
for (TestModule next : ServiceLoader.load(TestModule.class)) {
64+
testModules.add(next);
65+
}
66+
if (testModules.isEmpty()) {
67+
return buildFromResources();
68+
} else {
69+
return buildFromModules(testModules);
70+
}
71+
}
72+
73+
private BeanScope buildFromModules(List<TestModule> testModules) {
74+
return BeanScope.newBuilder()
75+
.withModules(testModules.toArray(Module[]::new))
76+
.withShutdownHook(shutdownHook)
77+
.build();
78+
}
79+
80+
/**
81+
* Fallback when ServiceLoader does not work in module-path for generated test service.
82+
*/
83+
@Nullable
84+
private BeanScope buildFromResources() {
85+
try {
86+
List<TestModule> testModules = new ArrayList<>();
87+
Enumeration<URL> urls = ClassLoader.getSystemResources("META-INF/services/io.avaje.inject.test.TestModule");
88+
while (urls.hasMoreElements()) {
89+
String className = readServiceClassName(urls.nextElement());
90+
if (className != null) {
91+
Class<?> cls = Class.forName(className);
92+
testModules.add((TestModule) cls.getDeclaredConstructor().newInstance());
93+
}
94+
}
95+
return testModules.isEmpty() ? null : buildFromModules(testModules);
96+
} catch (Throwable e) {
97+
throw new RuntimeException("Error trying to create TestModule", e);
98+
}
99+
}
100+
101+
@Nullable
102+
private String readServiceClassName(URL url) throws IOException {
103+
if (url != null) {
104+
InputStream is = url.openStream();
105+
if (is != null) {
106+
try (LineNumberReader lineNumberReader = new LineNumberReader(new InputStreamReader(is))) {
107+
return lineNumberReader.readLine();
108+
}
109+
}
110+
}
111+
return null;
112+
}
113+
114+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package io.avaje.inject.test;
2+
3+
import io.avaje.inject.BeanScope;
4+
import io.avaje.inject.BeanScopeBuilder;
5+
import io.avaje.lang.NonNullApi;
6+
import io.avaje.lang.Nullable;
7+
8+
/**
9+
* Provides access to the global "test scope" and helper methods to use it.
10+
*/
11+
@NonNullApi
12+
public abstract class TestBeanScope {
13+
14+
/**
15+
* Build and return a new BeanScope that will have the global "test scope" as its parent.
16+
* <p>
17+
* That is, beans created using {@code @TestScope} are all in the global "test scope"
18+
* which is a parent to this scope.
19+
*
20+
* @return A new test BeanScope with the global "test scope" as its parent.
21+
*/
22+
public static BeanScopeBuilder builder() {
23+
BeanScope globalTestScope = init(true);
24+
return BeanScope.newBuilder().withParent(globalTestScope, false);
25+
}
26+
27+
/**
28+
* Return the BeanScope for {@code @TestScope} beans ONLY building once.
29+
* <p>
30+
* If the BeanScope has already been created then that shared scope is returned.
31+
*
32+
* @return The test scope BeanScope (nullable).
33+
*/
34+
@Nullable
35+
public static BeanScope initialise() {
36+
return init(true);
37+
}
38+
39+
/**
40+
* Build and return a new BeanScope for {@code @TestScope} beans. This builds the beans
41+
* in the test scope every time it is called.
42+
* <p>
43+
* Generally, we DO NOT want to use this method but use {@link #initialise()} instead.
44+
*
45+
* @param shutdownHook Set whether a shutdown hook should be created to close the BeanScope.
46+
* @return The test scope BeanScope (nullable).
47+
*/
48+
@Nullable
49+
public static BeanScope create(boolean shutdownHook) {
50+
return TSBuild.create(shutdownHook);
51+
}
52+
53+
@Nullable
54+
static BeanScope init(boolean shutdownHook) {
55+
return TSBuild.initialise(shutdownHook);
56+
}
57+
58+
}

inject-test/src/main/java/io/avaje/inject/test/TestScope.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77
*/
88
@Scope
99
public @interface TestScope {
10+
1011
}

inject/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<parent>
55
<groupId>io.avaje</groupId>
66
<artifactId>avaje-inject-parent</artifactId>
7-
<version>8.2</version>
7+
<version>8.3</version>
88
</parent>
99

1010
<artifactId>avaje-inject</artifactId>

inject/src/main/java/io/avaje/inject/spi/DEntry.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ class DEntry implements BeanEntry {
2020
this.bean = bean;
2121
}
2222

23+
@Override
24+
public String toString() {
25+
return "{bean=" + bean
26+
+ (qualifierName == null ? "" : ", name='" + qualifierName + '\'')
27+
+ (keys.isEmpty() ? "" : ", keys=" + keys)
28+
+ ", priority=" + priority
29+
+ '}';
30+
}
31+
2332
void addKey(String key) {
2433
keys.add(key);
2534
}

0 commit comments

Comments
 (0)