Skip to content

Commit 4fe5b9a

Browse files
committed
For @InjectTest only build new BeanScope when test has mocks or spies
Currently, for each test using @InjectTest a new BeanScope is wired (and uses a parent scope with @TestScope beans in it). This changes, such that the "global test scope" is a pair of BeanScope with - "testBaseScope" as scope with all @TestScope beans in it - "testAllScope" as all beans (with the testBaseScope as its parent). Any test that just injects can reuse the "testAllScope" (and not write a new BeanScope). Any test that uses mocks, spies, has a setup method, or uses profiles must work as before and wire a new BeanScope (and as before use the "testBaseScope" as its parent). This change makes component testing faster
1 parent cf9563d commit 4fe5b9a

File tree

7 files changed

+145
-44
lines changed

7 files changed

+145
-44
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();
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: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ 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

@@ -42,7 +42,7 @@ static BeanScope initialise(boolean shutdownHook) {
4242
lock.lock();
4343
try {
4444
if (SCOPE == null) {
45-
SCOPE = create(shutdownHook);
45+
SCOPE = createTestBaseScope(shutdownHook);
4646
}
4747
return SCOPE;
4848
} finally {
@@ -54,6 +54,18 @@ static BeanScope initialise(boolean shutdownHook) {
5454
this.shutdownHook = shutdownHook;
5555
}
5656

57+
static GlobalTestScope.Pair initialise() {
58+
BeanScope testBaseScope = createTestBaseScope(false);
59+
BeanScope testAllScope = createTestAllScope(testBaseScope);
60+
return new GlobalTestScope.Pair(testAllScope, testBaseScope);
61+
}
62+
63+
private static BeanScope createTestAllScope(BeanScope testBaseScope) {
64+
return BeanScope.builder()
65+
.parent(testBaseScope, false)
66+
.build();
67+
}
68+
5769
@Nullable
5870
private BeanScope build() {
5971
List<TestModule> testModules = new ArrayList<>();

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public static BeanScope initialise() {
7474
*/
7575
@Nullable
7676
public static BeanScope create(boolean shutdownHook) {
77-
return TSBuild.create(shutdownHook);
77+
return TSBuild.createTestBaseScope(shutdownHook);
7878
}
7979

8080
@Nullable

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)