Skip to content

Commit 06915ff

Browse files
authored
Merge pull request #503 from avaje/feature/global-test-scope-pair
For @InjectTest only build new BeanScope when test has mocks or spies
2 parents cf9563d + 40f89a4 commit 06915ff

File tree

7 files changed

+149
-55
lines changed

7 files changed

+149
-55
lines changed

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

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ final class GlobalTestScope implements ExtensionContext.Store.CloseableResource
1818

1919
private final ReentrantLock lock = new ReentrantLock();
2020
private boolean started;
21-
private BeanScope globalBeanScope;
21+
private Pair globalBeanScope;
2222

23-
BeanScope obtain(ExtensionContext context) {
23+
Pair obtain(ExtensionContext context) {
2424
lock.lock();
2525
try {
2626
if (!started) {
@@ -34,11 +34,9 @@ BeanScope obtain(ExtensionContext context) {
3434
}
3535

3636
private void initialise(ExtensionContext context) {
37-
globalBeanScope = TestBeanScope.init(false);
38-
if (globalBeanScope != null) {
39-
log.log(TRACE, "register global test BeanScope with beans {0}", globalBeanScope);
40-
context.getRoot().getStore(ExtensionContext.Namespace.GLOBAL).put(InjectExtension.class.getCanonicalName(), this);
41-
}
37+
globalBeanScope = TSBuild.initialise(false);
38+
log.log(TRACE, "register global test BeanScope with beans {0}", globalBeanScope);
39+
context.getRoot().getStore(ExtensionContext.Namespace.GLOBAL).put(InjectExtension.class.getCanonicalName(), this);
4240
}
4341

4442
/**
@@ -57,4 +55,55 @@ public void close() {
5755
}
5856
}
5957

58+
/**
59+
* The pair of BeanScopes that can be used for InjectTests.
60+
*/
61+
static final class Pair {
62+
63+
/**
64+
* Entire application wired (with testScope as parent replacing those beans).
65+
* This can be used when a test only injects beans and there are no mocks,
66+
* spies, or setup methods.
67+
*/
68+
private final BeanScope allScope;
69+
70+
/**
71+
* The TestScope beans, used as the parent scope when a new BeanScope
72+
* needs to be wired for a test (due to mocks, spies or setup methods).
73+
*/
74+
private final BeanScope baseScope;
75+
76+
Pair(BeanScope allScope, BeanScope baseScope) {
77+
this.allScope = allScope;
78+
this.baseScope = baseScope;
79+
}
80+
81+
void close() {
82+
if (allScope != null) {
83+
allScope.close();
84+
}
85+
if (baseScope != null) {
86+
baseScope.close();
87+
}
88+
}
89+
90+
BeanScope allScope() {
91+
return allScope;
92+
}
93+
94+
BeanScope baseScope() {
95+
return baseScope;
96+
}
97+
98+
Pair newPair(BeanScope newAllScope) {
99+
return new Pair(newAllScope, baseScope);
100+
}
101+
102+
@Override
103+
public String toString() {
104+
return "All[" + allScope + "] Test[" + baseScope + "]";
105+
}
106+
107+
}
108+
60109
}

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public final class InjectExtension implements BeforeAllCallback, AfterAllCallbac
2121
private static final String META = "META";
2222
private static final GlobalTestScope GLOBAL = new GlobalTestScope();
2323

24-
private BeanScope globalBeanScope;
24+
private GlobalTestScope.Pair globalBeanScope;
2525

2626
@Override
2727
public void beforeAll(ExtensionContext context) {
@@ -64,9 +64,8 @@ public void beforeEach(final ExtensionContext context) {
6464
if (metaInfo.hasInstanceInjection()) {
6565

6666
// if (static fields) then (class scope) else (globalTestScope)
67-
final BeanScope parent = metaInfo.hasStaticInjection() ? getClassScope(context) : globalBeanScope;
68-
69-
AutoCloseable beanScope = metaInfo.buildForInstance(parent, context.getRequiredTestInstance());
67+
final GlobalTestScope.Pair pair = metaInfo.hasStaticInjection() ? globalBeanScope.newPair(getClassScope(context)) : globalBeanScope;
68+
AutoCloseable beanScope = metaInfo.buildForInstance(pair, context.getRequiredTestInstance());
7069

7170
// put method level test scope
7271
Method testMethod = context.getRequiredTestMethod();

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

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,38 +27,48 @@ boolean hasInstanceInjection() {
2727
/**
2828
* Build for static fields class level scope.
2929
*/
30-
Scope buildForClass(BeanScope globalTestScope) {
30+
Scope buildForClass(GlobalTestScope.Pair globalTestScope) {
3131
return buildSet(globalTestScope, null);
3232
}
3333

3434
/**
3535
* Build test instance per test scope.
3636
*/
37-
Scope buildForInstance(BeanScope globalTestScope, Object testInstance) {
37+
Scope buildForInstance(GlobalTestScope.Pair globalTestScope, Object testInstance) {
3838
return buildSet(globalTestScope, testInstance);
3939
}
4040

41-
private Scope buildSet(BeanScope parent, Object testInstance) {
42-
final BeanScopeBuilder builder = BeanScope.builder();
43-
if (parent != null) {
44-
builder.parent(parent, false);
41+
private Scope buildSet(GlobalTestScope.Pair parent, Object testInstance) {
42+
// wiring profiles
43+
String[] profiles = Optional.ofNullable(testInstance)
44+
.map(Object::getClass)
45+
.map(c -> c.getAnnotation(InjectTest.class))
46+
.map(InjectTest::profiles)
47+
.orElse(new String[0]);
48+
49+
boolean newScope = false;
50+
final BeanScope beanScope;
51+
if (profiles.length > 0 || reader.hasMocksOrSpies(testInstance)) {
52+
// need to build a BeanScope for this using testScope() as the parent
53+
final BeanScopeBuilder builder = BeanScope.builder();
54+
if (parent != null) {
55+
builder.parent(parent.baseScope(), false);
56+
if (profiles.length > 0) {
57+
builder.profiles(profiles);
58+
}
59+
}
60+
// register mocks and spies local to this test
61+
reader.build(builder, testInstance);
62+
// wire with local mocks, spies, and globalTestScope
63+
beanScope = builder.build();
64+
newScope = true;
65+
} else {
66+
// just use the all scope
67+
beanScope = parent.allScope();
4568
}
4669

47-
//set wiring profile
48-
Optional.ofNullable(testInstance)
49-
.map(Object::getClass)
50-
.map(c -> c.getAnnotation(InjectTest.class))
51-
.map(InjectTest::profiles)
52-
.ifPresent(builder::profiles);
53-
54-
// register mocks and spies local to this test
55-
reader.build(builder, testInstance);
56-
57-
// wire with local mocks, spies, and globalTestScope
58-
final BeanScope beanScope = builder.build();
59-
6070
// set inject, spy, mock fields from beanScope
61-
return reader.setFromScope(beanScope, testInstance);
71+
return reader.setFromScope(beanScope, testInstance, newScope);
6272
}
6373

6474
/**
@@ -69,10 +79,12 @@ static class Scope implements AutoCloseable {
6979

7080
private final BeanScope beanScope;
7181
private final Plugin.Scope pluginScope;
82+
private final boolean newScope;
7283

73-
Scope(BeanScope beanScope, Plugin.Scope pluginScope) {
84+
Scope(BeanScope beanScope, Plugin.Scope pluginScope, boolean newScope) {
7485
this.beanScope = beanScope;
7586
this.pluginScope = pluginScope;
87+
this.newScope = newScope;
7688
}
7789

7890
BeanScope beanScope() {
@@ -81,7 +93,9 @@ BeanScope beanScope() {
8193

8294
@Override
8395
public void close() {
84-
beanScope.close();
96+
if (newScope) {
97+
beanScope.close();
98+
}
8599
if (pluginScope != null) {
86100
pluginScope.close();
87101
}

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

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,33 @@ final class MetaReader {
4848
}
4949
}
5050

51+
boolean hasMocksOrSpies(Object testInstance) {
52+
if (testInstance == null) {
53+
return hasStaticMocksOrSpies() || methodFinder.hasStaticMethods();
54+
} else {
55+
return hasInstanceMocksOrSpies(testInstance) || methodFinder.hasInstanceMethods();
56+
}
57+
}
58+
59+
private boolean hasInstanceMocksOrSpies(Object testInstance) {
60+
return !mocks.isEmpty() || !spies.isEmpty() || hasInjectMock(injection, testInstance);
61+
}
62+
63+
private boolean hasStaticMocksOrSpies() {
64+
return !staticMocks.isEmpty() || !staticSpies.isEmpty() || hasInjectMock(staticInjection, null);
65+
}
66+
67+
private boolean hasInjectMock(List<FieldTarget> fields, Object testInstance) {
68+
for (FieldTarget target : fields) {
69+
Object existingValue = target.get(testInstance);
70+
if (existingValue != null) {
71+
// an assigned injection field is a mock
72+
return true;
73+
}
74+
}
75+
return false;
76+
}
77+
5178
private static LinkedList<Class<?>> typeHierarchy(Class<?> testClass) {
5279
var hierarchy = new LinkedList<Class<?>>();
5380
var analyzedClass = testClass;
@@ -142,15 +169,15 @@ private String name(Field field) {
142169
return null;
143170
}
144171

145-
MetaInfo.Scope setFromScope(BeanScope beanScope, Object testInstance) {
172+
MetaInfo.Scope setFromScope(BeanScope beanScope, Object testInstance, boolean newScope) {
146173
if (testInstance != null) {
147-
return setForInstance(beanScope, testInstance);
174+
return setForInstance(beanScope, testInstance, newScope);
148175
} else {
149-
return setForStatics(beanScope);
176+
return setForStatics(beanScope, newScope);
150177
}
151178
}
152179

153-
private MetaInfo.Scope setForInstance(BeanScope beanScope, Object testInstance) {
180+
private MetaInfo.Scope setForInstance(BeanScope beanScope, Object testInstance, boolean newScope) {
154181
try {
155182
Plugin.Scope pluginScope = instancePlugin ? plugin.createScope(beanScope) : null;
156183

@@ -171,14 +198,14 @@ private MetaInfo.Scope setForInstance(BeanScope beanScope, Object testInstance)
171198
target.setFromScope(beanScope, testInstance);
172199
}
173200
}
174-
return new MetaInfo.Scope(beanScope, pluginScope);
201+
return new MetaInfo.Scope(beanScope, pluginScope, newScope);
175202

176203
} catch (IllegalAccessException e) {
177204
throw new RuntimeException(e);
178205
}
179206
}
180207

181-
private MetaInfo.Scope setForStatics(BeanScope beanScope) {
208+
private MetaInfo.Scope setForStatics(BeanScope beanScope, boolean newScope) {
182209
try {
183210
Plugin.Scope pluginScope = staticPlugin ? plugin.createScope(beanScope) : null;
184211

@@ -196,7 +223,7 @@ private MetaInfo.Scope setForStatics(BeanScope beanScope) {
196223
target.setFromScope(beanScope, null);
197224
}
198225
}
199-
return new MetaInfo.Scope(beanScope, pluginScope);
226+
return new MetaInfo.Scope(beanScope, pluginScope, newScope);
200227
} catch (IllegalAccessException e) {
201228
throw new RuntimeException(e);
202229
}

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
final class TSBuild {
2222

2323
private static final ReentrantLock lock = new ReentrantLock();
24-
private static BeanScope SCOPE;
24+
private static GlobalTestScope.Pair SCOPE;
2525

2626
private final boolean shutdownHook;
2727

@@ -30,19 +30,18 @@ final class TSBuild {
3030
* time this method is called.
3131
*/
3232
@Nullable
33-
static BeanScope create(boolean shutdownHook) {
33+
static BeanScope createTestBaseScope(boolean shutdownHook) {
3434
return new TSBuild(shutdownHook).build();
3535
}
3636

3737
/**
3838
* Return the test BeanScope only creating once.
3939
*/
40-
@Nullable
41-
static BeanScope initialise(boolean shutdownHook) {
40+
static GlobalTestScope.Pair initialise(boolean shutdownHook) {
4241
lock.lock();
4342
try {
4443
if (SCOPE == null) {
45-
SCOPE = create(shutdownHook);
44+
SCOPE = createScopes(shutdownHook);
4645
}
4746
return SCOPE;
4847
} finally {
@@ -54,6 +53,18 @@ static BeanScope initialise(boolean shutdownHook) {
5453
this.shutdownHook = shutdownHook;
5554
}
5655

56+
private static GlobalTestScope.Pair createScopes(boolean shutdownHook) {
57+
BeanScope testBaseScope = createTestBaseScope(shutdownHook);
58+
BeanScope testAllScope = createTestAllScope(testBaseScope);
59+
return new GlobalTestScope.Pair(testAllScope, testBaseScope);
60+
}
61+
62+
private static BeanScope createTestAllScope(BeanScope testBaseScope) {
63+
return BeanScope.builder()
64+
.parent(testBaseScope, false)
65+
.build();
66+
}
67+
5768
@Nullable
5869
private BeanScope build() {
5970
List<TestModule> testModules = new ArrayList<>();

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

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ public abstract class TestBeanScope {
3838
* @return A new test BeanScope with the global "test scope" as its parent.
3939
*/
4040
public static BeanScopeBuilder builder() {
41-
BeanScope globalTestScope = init(true);
42-
return BeanScope.builder().parent(globalTestScope, false);
41+
return BeanScope.builder().parent(initialise(), false);
4342
}
4443

4544
/**
@@ -60,7 +59,7 @@ public static BeanScopeBuilder builder() {
6059
*/
6160
@Nullable
6261
public static BeanScope initialise() {
63-
return init(true);
62+
return TSBuild.initialise(true).baseScope();
6463
}
6564

6665
/**
@@ -74,12 +73,7 @@ public static BeanScope initialise() {
7473
*/
7574
@Nullable
7675
public static BeanScope create(boolean shutdownHook) {
77-
return TSBuild.create(shutdownHook);
78-
}
79-
80-
@Nullable
81-
static BeanScope init(boolean shutdownHook) {
82-
return TSBuild.initialise(shutdownHook);
76+
return TSBuild.createTestBaseScope(shutdownHook);
8377
}
8478

8579
}

inject-test/src/test/java/io/avaje/inject/test/MetaReaderTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ void checkMetaReader_with_plugin() {
3939
assertThat(metaReader.instancePlugin).isTrue();
4040

4141
HelloBean helloBean = new HelloBean();
42-
metaReader.setFromScope(Mockito.mock(BeanScope.class), helloBean);
42+
metaReader.setFromScope(Mockito.mock(BeanScope.class), helloBean, true);
4343

4444
assertThat(helloBean.client).isNotNull();
4545
}

0 commit comments

Comments
 (0)