Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

For @InjectTest only build new BeanScope when test has mocks or spies #503

Merged
merged 3 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ final class GlobalTestScope implements ExtensionContext.Store.CloseableResource

private final ReentrantLock lock = new ReentrantLock();
private boolean started;
private BeanScope globalBeanScope;
private Pair globalBeanScope;

BeanScope obtain(ExtensionContext context) {
Pair obtain(ExtensionContext context) {
lock.lock();
try {
if (!started) {
Expand All @@ -34,11 +34,9 @@ BeanScope obtain(ExtensionContext context) {
}

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

/**
Expand All @@ -57,4 +55,55 @@ public void close() {
}
}

/**
* The pair of BeanScopes that can be used for InjectTests.
*/
static final class Pair {

/**
* Entire application wired (with testScope as parent replacing those beans).
* This can be used when a test only injects beans and there are no mocks,
* spies, or setup methods.
*/
private final BeanScope allScope;

/**
* The TestScope beans, used as the parent scope when a new BeanScope
* needs to be wired for a test (due to mocks, spies or setup methods).
*/
private final BeanScope baseScope;

Pair(BeanScope allScope, BeanScope baseScope) {
this.allScope = allScope;
this.baseScope = baseScope;
}

void close() {
if (allScope != null) {
allScope.close();
}
if (baseScope != null) {
baseScope.close();
}
}

BeanScope allScope() {
return allScope;
}

BeanScope baseScope() {
return baseScope;
}

Pair newPair(BeanScope newAllScope) {
return new Pair(newAllScope, baseScope);
}

@Override
public String toString() {
return "All[" + allScope + "] Test[" + baseScope + "]";
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public final class InjectExtension implements BeforeAllCallback, AfterAllCallbac
private static final String META = "META";
private static final GlobalTestScope GLOBAL = new GlobalTestScope();

private BeanScope globalBeanScope;
private GlobalTestScope.Pair globalBeanScope;

@Override
public void beforeAll(ExtensionContext context) {
Expand Down Expand Up @@ -64,9 +64,8 @@ public void beforeEach(final ExtensionContext context) {
if (metaInfo.hasInstanceInjection()) {

// if (static fields) then (class scope) else (globalTestScope)
final BeanScope parent = metaInfo.hasStaticInjection() ? getClassScope(context) : globalBeanScope;

AutoCloseable beanScope = metaInfo.buildForInstance(parent, context.getRequiredTestInstance());
final GlobalTestScope.Pair pair = metaInfo.hasStaticInjection() ? globalBeanScope.newPair(getClassScope(context)) : globalBeanScope;
AutoCloseable beanScope = metaInfo.buildForInstance(pair, context.getRequiredTestInstance());

// put method level test scope
Method testMethod = context.getRequiredTestMethod();
Expand Down
58 changes: 36 additions & 22 deletions inject-test/src/main/java/io/avaje/inject/test/MetaInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,38 +27,48 @@ boolean hasInstanceInjection() {
/**
* Build for static fields class level scope.
*/
Scope buildForClass(BeanScope globalTestScope) {
Scope buildForClass(GlobalTestScope.Pair globalTestScope) {
return buildSet(globalTestScope, null);
}

/**
* Build test instance per test scope.
*/
Scope buildForInstance(BeanScope globalTestScope, Object testInstance) {
Scope buildForInstance(GlobalTestScope.Pair globalTestScope, Object testInstance) {
return buildSet(globalTestScope, testInstance);
}

private Scope buildSet(BeanScope parent, Object testInstance) {
final BeanScopeBuilder builder = BeanScope.builder();
if (parent != null) {
builder.parent(parent, false);
private Scope buildSet(GlobalTestScope.Pair parent, Object testInstance) {
// wiring profiles
String[] profiles = Optional.ofNullable(testInstance)
.map(Object::getClass)
.map(c -> c.getAnnotation(InjectTest.class))
.map(InjectTest::profiles)
.orElse(new String[0]);

boolean newScope = false;
final BeanScope beanScope;
if (profiles.length > 0 || reader.hasMocksOrSpies(testInstance)) {
// need to build a BeanScope for this using testScope() as the parent
final BeanScopeBuilder builder = BeanScope.builder();
if (parent != null) {
builder.parent(parent.baseScope(), false);
if (profiles.length > 0) {
builder.profiles(profiles);
}
}
// register mocks and spies local to this test
reader.build(builder, testInstance);
// wire with local mocks, spies, and globalTestScope
beanScope = builder.build();
newScope = true;
} else {
// just use the all scope
beanScope = parent.allScope();
}

//set wiring profile
Optional.ofNullable(testInstance)
.map(Object::getClass)
.map(c -> c.getAnnotation(InjectTest.class))
.map(InjectTest::profiles)
.ifPresent(builder::profiles);

// register mocks and spies local to this test
reader.build(builder, testInstance);

// wire with local mocks, spies, and globalTestScope
final BeanScope beanScope = builder.build();

// set inject, spy, mock fields from beanScope
return reader.setFromScope(beanScope, testInstance);
return reader.setFromScope(beanScope, testInstance, newScope);
}

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

private final BeanScope beanScope;
private final Plugin.Scope pluginScope;
private final boolean newScope;

Scope(BeanScope beanScope, Plugin.Scope pluginScope) {
Scope(BeanScope beanScope, Plugin.Scope pluginScope, boolean newScope) {
this.beanScope = beanScope;
this.pluginScope = pluginScope;
this.newScope = newScope;
}

BeanScope beanScope() {
Expand All @@ -81,7 +93,9 @@ BeanScope beanScope() {

@Override
public void close() {
beanScope.close();
if (newScope) {
beanScope.close();
}
if (pluginScope != null) {
pluginScope.close();
}
Expand Down
41 changes: 34 additions & 7 deletions inject-test/src/main/java/io/avaje/inject/test/MetaReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,33 @@ final class MetaReader {
}
}

boolean hasMocksOrSpies(Object testInstance) {
if (testInstance == null) {
return hasStaticMocksOrSpies() || methodFinder.hasStaticMethods();
} else {
return hasInstanceMocksOrSpies(testInstance) || methodFinder.hasInstanceMethods();
}
}

private boolean hasInstanceMocksOrSpies(Object testInstance) {
return !mocks.isEmpty() || !spies.isEmpty() || hasInjectMock(injection, testInstance);
}

private boolean hasStaticMocksOrSpies() {
return !staticMocks.isEmpty() || !staticSpies.isEmpty() || hasInjectMock(staticInjection, null);
}

private boolean hasInjectMock(List<FieldTarget> fields, Object testInstance) {
for (FieldTarget target : fields) {
Object existingValue = target.get(testInstance);
if (existingValue != null) {
// an assigned injection field is a mock
return true;
}
}
return false;
}

private static LinkedList<Class<?>> typeHierarchy(Class<?> testClass) {
var hierarchy = new LinkedList<Class<?>>();
var analyzedClass = testClass;
Expand Down Expand Up @@ -142,15 +169,15 @@ private String name(Field field) {
return null;
}

MetaInfo.Scope setFromScope(BeanScope beanScope, Object testInstance) {
MetaInfo.Scope setFromScope(BeanScope beanScope, Object testInstance, boolean newScope) {
if (testInstance != null) {
return setForInstance(beanScope, testInstance);
return setForInstance(beanScope, testInstance, newScope);
} else {
return setForStatics(beanScope);
return setForStatics(beanScope, newScope);
}
}

private MetaInfo.Scope setForInstance(BeanScope beanScope, Object testInstance) {
private MetaInfo.Scope setForInstance(BeanScope beanScope, Object testInstance, boolean newScope) {
try {
Plugin.Scope pluginScope = instancePlugin ? plugin.createScope(beanScope) : null;

Expand All @@ -171,14 +198,14 @@ private MetaInfo.Scope setForInstance(BeanScope beanScope, Object testInstance)
target.setFromScope(beanScope, testInstance);
}
}
return new MetaInfo.Scope(beanScope, pluginScope);
return new MetaInfo.Scope(beanScope, pluginScope, newScope);

} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}

private MetaInfo.Scope setForStatics(BeanScope beanScope) {
private MetaInfo.Scope setForStatics(BeanScope beanScope, boolean newScope) {
try {
Plugin.Scope pluginScope = staticPlugin ? plugin.createScope(beanScope) : null;

Expand All @@ -196,7 +223,7 @@ private MetaInfo.Scope setForStatics(BeanScope beanScope) {
target.setFromScope(beanScope, null);
}
}
return new MetaInfo.Scope(beanScope, pluginScope);
return new MetaInfo.Scope(beanScope, pluginScope, newScope);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
Expand Down
21 changes: 16 additions & 5 deletions inject-test/src/main/java/io/avaje/inject/test/TSBuild.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
final class TSBuild {

private static final ReentrantLock lock = new ReentrantLock();
private static BeanScope SCOPE;
private static GlobalTestScope.Pair SCOPE;

private final boolean shutdownHook;

Expand All @@ -30,19 +30,18 @@ final class TSBuild {
* time this method is called.
*/
@Nullable
static BeanScope create(boolean shutdownHook) {
static BeanScope createTestBaseScope(boolean shutdownHook) {
return new TSBuild(shutdownHook).build();
}

/**
* Return the test BeanScope only creating once.
*/
@Nullable
static BeanScope initialise(boolean shutdownHook) {
static GlobalTestScope.Pair initialise(boolean shutdownHook) {
lock.lock();
try {
if (SCOPE == null) {
SCOPE = create(shutdownHook);
SCOPE = createScopes(shutdownHook);
}
return SCOPE;
} finally {
Expand All @@ -54,6 +53,18 @@ static BeanScope initialise(boolean shutdownHook) {
this.shutdownHook = shutdownHook;
}

private static GlobalTestScope.Pair createScopes(boolean shutdownHook) {
BeanScope testBaseScope = createTestBaseScope(shutdownHook);
BeanScope testAllScope = createTestAllScope(testBaseScope);
return new GlobalTestScope.Pair(testAllScope, testBaseScope);
}

private static BeanScope createTestAllScope(BeanScope testBaseScope) {
return BeanScope.builder()
.parent(testBaseScope, false)
.build();
}

@Nullable
private BeanScope build() {
List<TestModule> testModules = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ public abstract class TestBeanScope {
* @return A new test BeanScope with the global "test scope" as its parent.
*/
public static BeanScopeBuilder builder() {
BeanScope globalTestScope = init(true);
return BeanScope.builder().parent(globalTestScope, false);
return BeanScope.builder().parent(initialise(), false);
}

/**
Expand All @@ -60,7 +59,7 @@ public static BeanScopeBuilder builder() {
*/
@Nullable
public static BeanScope initialise() {
return init(true);
return TSBuild.initialise(true).baseScope();
}

/**
Expand All @@ -74,12 +73,7 @@ public static BeanScope initialise() {
*/
@Nullable
public static BeanScope create(boolean shutdownHook) {
return TSBuild.create(shutdownHook);
}

@Nullable
static BeanScope init(boolean shutdownHook) {
return TSBuild.initialise(shutdownHook);
return TSBuild.createTestBaseScope(shutdownHook);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ void checkMetaReader_with_plugin() {
assertThat(metaReader.instancePlugin).isTrue();

HelloBean helloBean = new HelloBean();
metaReader.setFromScope(Mockito.mock(BeanScope.class), helloBean);
metaReader.setFromScope(Mockito.mock(BeanScope.class), helloBean, true);

assertThat(helloBean.client).isNotNull();
}
Expand Down
Loading