Skip to content
Open
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
@@ -0,0 +1,24 @@
package org.multi.main;

import io.avaje.inject.test.InjectTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import org.multi.parents.BeanIn1;
import org.multi.parents.Mod1Module;

import static org.junit.jupiter.api.Assertions.assertNotNull;

/**
* Bound the wiring to a custom scope module. Custom scope modules are not registered as an {@code
* InjectExtension} service, so this covers the reflection fallback in {@code SelectedModules}.
*/
@InjectTest(modules = Mod1Module.class)
class BoundedCustomModuleTest {

@Inject BeanIn1 beanIn1;

@Test
void onlyTheSelectedModuleIsWired() {
assertNotNull(beanIn1);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.example.myapp.modules;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.Optional;

import org.example.external.aspect.sub.ExampleExternalAspectModule;
import org.example.myapp.HelloService;
import org.junit.jupiter.api.Test;

import io.avaje.inject.aop.MethodInterceptor;
import io.avaje.inject.test.InjectTest;
import jakarta.inject.Inject;

/**
* The selected modules must also bound the class level BeanScope, which is built without a test
* instance - so {@code MetaInfo} has to read them from the test class.
*/
@InjectTest(modules = ExampleExternalAspectModule.class)
class BoundedModulesStaticTest {

@Inject static MethodInterceptor interceptor;

@Inject static Optional<HelloService> helloService;

@Test
void classLevelScopeIsAlsoBounded() {
assertThat(interceptor).isNotNull();
assertThat(helloService).isEmpty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package org.example.myapp.modules;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.Optional;

import org.example.external.aspect.sub.ExampleExternalAspectModule;
import org.example.myapp.HelloData;
import org.example.myapp.HelloService;
import org.junit.jupiter.api.Test;
import org.other.one.OtherComponent;

import io.avaje.inject.aop.MethodInterceptor;
import io.avaje.inject.test.InjectTest;
import jakarta.inject.Inject;

/**
* Bound the wiring to a single module. {@code ExampleExternalAspectModule} is registered as an
* {@code InjectExtension} service so this covers the ServiceLoader path in {@code SelectedModules}.
*/
@InjectTest(modules = ExampleExternalAspectModule.class)
class BoundedModulesTest {

/** Provided by the selected module. */
@Inject MethodInterceptor interceptor;

/** Provided by the global test scope (parent), so still available. */
@Inject HelloData helloData;

/** Provided by MyappModule, which was not selected. */
@Inject Optional<HelloService> helloService;

/** Provided by OneModule (blackbox-other), which was not selected. */
@Inject Optional<OtherComponent> otherComponent;

@Test
void selectedModuleIsWired() {
assertThat(interceptor).isNotNull();
}

@Test
void parentTestScopeIsStillInherited() {
assertThat(helloData.helloData()).isEqualTo("TestHelloData");
}

@Test
void unselectedModulesAreNotWired() {
assertThat(helloService).isEmpty();
assertThat(otherComponent).isEmpty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,7 @@ static GlobalTestBeans.Beans initialise(boolean shutdownHook) {
}

private static GlobalTestBeans.Beans createScopes(boolean shutdownHook) {
BeanScope testBaseScope = createTestBaseScope(shutdownHook);
BeanScope testAllScope = createTestAllScope(testBaseScope);
Plugin.Scope pluginAll = PluginMgr.scope(testAllScope);
return new GlobalTestBeans.Beans(pluginAll, testAllScope, testBaseScope);
}

private static BeanScope createTestAllScope(BeanScope testBaseScope) {
return BeanScope.builder()
.parent(testBaseScope, false)
.build();
return new GlobalTestBeans.Beans(createTestBaseScope(shutdownHook));
}

@Nullable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,21 +61,30 @@ public void close() {
*/
static final class Beans {

private final Plugin.Scope plugin;
private final ReentrantLock allBeansLock = new ReentrantLock();

private Plugin.Scope plugin;

/**
* 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.
* <p>
* Wired lazily on first use as a test using {@code @InjectTest(modules = ...)}
* only wires the modules it selects and never uses this.
*/
private final BeanScope allBeans;
private BeanScope allBeans;

/**
* 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 baseBeans;

Beans(BeanScope baseBeans) {
this.baseBeans = baseBeans;
}

Beans(Plugin.Scope plugin, BeanScope allBeans, BeanScope baseBeans) {
this.plugin = plugin;
this.allBeans = allBeans;
Expand All @@ -95,11 +104,33 @@ void close() {
}

Plugin.Scope allPlugin() {
return plugin;
allBeansLock.lock();
try {
initAllBeans();
return plugin;
} finally {
allBeansLock.unlock();
}
}

BeanScope allBeans() {
return allBeans;
allBeansLock.lock();
try {
initAllBeans();
return allBeans;
} finally {
allBeansLock.unlock();
}
}

private void initAllBeans() {
if (allBeans == null) {
log.log(DEBUG, "Wiring all beans for the test BeanScope");
allBeans = BeanScope.builder()
.parent(baseBeans, false)
.build();
plugin = PluginMgr.scope(allBeans);
}
}

BeanScope baseBeans() {
Expand Down
10 changes: 10 additions & 0 deletions inject-test/src/main/java/io/avaje/inject/test/InjectTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

import org.junit.jupiter.api.extension.ExtendWith;

import io.avaje.inject.spi.AvajeModule;

/**
* An avaje-inject test supporting {@code @Inject} along with Mockito annotations -
* {@code @Mock, @Spy, @Captor}.
Expand All @@ -23,4 +25,12 @@

/** Create a new test beanscope for each test method */
boolean scopePerTest() default false;

/**
* Limit the wiring to only these modules, rather than every {@code AvajeModule} found on the test
* classpath.
*
* <p>This is the equivalent of {@code BeanScope.builder().modules(...)}
*/
Class<? extends AvajeModule>[] modules() default {};
}
40 changes: 29 additions & 11 deletions inject-test/src/main/java/io/avaje/inject/test/MetaInfo.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,41 @@
package io.avaje.inject.test;

import java.util.Optional;

import io.avaje.inject.BeanScope;
import io.avaje.inject.BeanScopeBuilder;
import io.avaje.inject.spi.AvajeModule;

/**
* Wraps the underlying metadata (fields with annotations @Mock, @Spy, @Inject, @Captor).
*/
final class MetaInfo {

@SuppressWarnings("unchecked")
private static final Class<? extends AvajeModule>[] NO_MODULES = new Class[0];

private final MetaReader reader;
private final Class<? extends AvajeModule>[] modules;
private final String[] profiles;
private final boolean scopePerTest;

MetaInfo(Class<?> testClass, Plugin plugin) {
this.reader = new MetaReader(testClass, plugin);
// read from the test class (walking enclosing classes for @Nested tests) rather than
// the instance so that the selected modules also bound the class level scope of a test
// that only has static injection
final InjectTest injectTest = injectTestAnnotation(testClass);
this.modules = injectTest == null ? NO_MODULES : injectTest.modules();
Comment thread
SentryMan marked this conversation as resolved.
this.profiles = injectTest == null ? new String[0] : injectTest.profiles();
this.scopePerTest = injectTest != null && injectTest.scopePerTest();
}

private static InjectTest injectTestAnnotation(Class<?> testClass) {
for (Class<?> c = testClass; c != null; c = c.getEnclosingClass()) {
final InjectTest injectTest = c.getAnnotation(InjectTest.class);
if (injectTest != null) {
return injectTest;
}
}
return null;
}

boolean hasStaticInjection() {
Expand Down Expand Up @@ -45,16 +67,9 @@ private TestBeans buildSet(GlobalTestBeans.Beans parent, Object testInstance) {
}

private TestBeans buildTestBeans(GlobalTestBeans.Beans parent, Object testInstance) {
var injectTest =
Optional.ofNullable(testInstance)
.map(Object::getClass)
.map(c -> c.getAnnotation(InjectTest.class));

// wiring profiles
String[] profiles = injectTest.map(InjectTest::profiles).orElse(new String[0]);

if (profiles.length > 0
|| injectTest.map(InjectTest::scopePerTest).orElse(false)
|| modules.length > 0
|| scopePerTest
|| reader.hasMocksOrSpies(testInstance)) {
// need to build a BeanScope for this using baseBeans() as the parent
final BeanScopeBuilder builder = BeanScope.builder();
Expand All @@ -64,6 +79,9 @@ private TestBeans buildTestBeans(GlobalTestBeans.Beans parent, Object testInstan
builder.profiles(profiles);
}
}
if (modules.length > 0) {
builder.modules(SelectedModules.instances(modules));
}
// register mocks and spies local to this test
reader.build(builder, testInstance);
// wire with local mocks, spies, and TestScope beans
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.avaje.inject.test;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.ServiceLoader;
import io.avaje.inject.spi.AvajeModule;
import io.avaje.inject.spi.InjectExtension;

/** Resolves the {@code AvajeModule} instances for {@code @InjectTest(modules = ...)}. */
final class SelectedModules {

private static final AvajeModule[] EMPTY = {};

private SelectedModules() {}

/** Return the module instances for the given module classes. */
static AvajeModule[] instances(Class<? extends AvajeModule>[] moduleClasses) {
final var wanted = new LinkedHashSet<>(List.of(moduleClasses));
final List<AvajeModule> modules = new ArrayList<>(wanted.size());
ServiceLoader.load(InjectExtension.class).stream()
.filter(provider -> wanted.contains(provider.type()))
.forEach(
provider -> {
wanted.remove(provider.type());
modules.add((AvajeModule) provider.get());
});
// fall back to reflection if the module is not registered as a service.
for (var cls : wanted) {
modules.add(newInstance(cls));
}
return modules.toArray(EMPTY);
}

private static AvajeModule newInstance(Class<? extends AvajeModule> cls) {
try {
return cls.getDeclaredConstructor().newInstance();
} catch (ReflectiveOperationException e) {
throw new IllegalStateException(
"Failed to create @InjectTest(modules = " + cls.getTypeName() + ")", e);
}
}
}
1 change: 1 addition & 0 deletions inject-test/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
requires static io.avaje.inject.aop;
requires static io.avaje.inject.events;

uses io.avaje.inject.spi.InjectExtension;
uses io.avaje.inject.test.TestModule;
uses io.avaje.inject.test.Plugin;
uses io.avaje.inject.test.LookupProvider;
Expand Down
Loading