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

Cached injector for common modules #3

Closed
wants to merge 5 commits into from
Closed
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 @@ -2,6 +2,7 @@

import static java.util.stream.Collectors.toSet;
import static org.junit.platform.commons.support.AnnotationSupport.findRepeatableAnnotations;
import static org.junit.platform.commons.support.AnnotationSupport.isAnnotated;

import com.google.common.collect.Iterables;
import com.google.common.reflect.TypeToken;
Expand All @@ -25,6 +26,8 @@
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Stream;
import javax.inject.Qualifier;
import org.junit.jupiter.api.extension.ExtensionContext;
Expand All @@ -39,6 +42,8 @@ public final class GuiceExtension implements TestInstancePostProcessor, Paramete
private static final Namespace NAMESPACE =
Namespace.create("name", "falgout", "jeffrey", "testing", "junit", "guice");

private static final ConcurrentMap<Set<? extends Class<?>>, Injector> INJECTOR_CACHE = new ConcurrentHashMap<>();

public GuiceExtension() {}

@Override
Expand All @@ -59,16 +64,22 @@ private static Optional<Injector> getOrCreateInjector(ExtensionContext context)
if (!context.getElement().isPresent()) {
return Optional.empty();
}

AnnotatedElement element = context.getElement().get();
Store store = context.getStore(NAMESPACE);

Injector injector = store.get(element, Injector.class);
boolean sharedInjector = isSharedInjector(context);
Set<Class<? extends Module>> moduleClasses = Collections.emptySet();
if (injector == null && sharedInjector) {
moduleClasses = getContextModuleTypes(context);
injector = INJECTOR_CACHE.get(moduleClasses);
}
if (injector == null) {
injector = createInjector(context);
store.put(element, injector);
if (sharedInjector && !moduleClasses.isEmpty()) {
INJECTOR_CACHE.put(moduleClasses, injector);
}
}

return Optional.of(injector);
}

Expand All @@ -79,12 +90,19 @@ private static Injector createInjector(ExtensionContext context)
InvocationTargetException {
Optional<Injector> parentInjector = getParentInjector(context);
List<? extends Module> modules = getNewModules(context);

return parentInjector
.map(injector -> injector.createChildInjector(modules))
.orElseGet(() -> Guice.createInjector(modules));
}

private static boolean isSharedInjector(ExtensionContext context) {
if (!context.getElement().isPresent()) {
return false;
}
AnnotatedElement element = context.getElement().get();
return isAnnotated(element, SharedInjectors.class);
}

private static Optional<Injector> getParentInjector(ExtensionContext context)
throws NoSuchMethodException,
InstantiationException,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package name.falgout.jeffrey.testing.junit.guice;

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

import java.lang.annotation.Documented;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Documented
@Retention(RUNTIME)
@Target({TYPE, METHOD})
@Inherited
@ExtendWith(GuiceExtension.class)
public @interface SharedInjectors {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package name.falgout.jeffrey.testing.junit.guice;

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

import com.google.inject.AbstractModule;
import com.google.inject.name.Names;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import javax.inject.Named;
import name.falgout.jeffrey.testing.junit.guice.SharedInjectorsTest.OuterClassModule;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(GuiceExtension.class)
@IncludeModule(OuterClassModule.class)
@SharedInjectors
class SharedInjectorsTest {

@Test
@IncludeModule(TestCaseModule.class)
@SharedInjectors
void test1(@Named("base") String base, @Named("test") String test) {
assertEquals("base", base);
assertEquals("test", test);
}

@Test
@SharedInjectors
@IncludeModule(OuterSharedClassModule.class)
void testCase2(int value) {
assertEquals(1, value);
}


@IncludeModule(InnerClassModule.class)
@Nested
class InnerClass {

@SharedInjectors
@IncludeModule(TestCaseModule.class)
@Test
void test1(@Named("base") String base,
@Named("inner") String inner,
@Named("test") String test) {
assertEquals("base", base);
assertEquals("test", test);
assertEquals("inner", inner);
}
}

@Nested
@IncludeModule(CachedModule.class)
@SharedInjectors
class FirstCachedInjectorTest {

@Test
void firstTest(long i) {
assertEquals(1, i);
}

@Test
void secondTest(long i) {
assertEquals(1, i);
}
}

@Nested
@IncludeModule(CachedModule.class)
@SharedInjectors
class SecondCachedInjectorTest {

@Test
void firstTest(long i) {
assertEquals(1, i);
}

@Test
void secondTest(long i) {
assertEquals(1, i);
}
}

@Nested
@IncludeModule(NonCachedModule.class)
class FirstNonCachedInjectorTest {

@Test
void test(long i) {
long expectedValue = NonCachedModule.SECOND_EXECUTED.get() ? 2 : 1;
assertEquals(expectedValue, i);
NonCachedModule.FIRST_EXECUTED.set(true);
}
}

@Nested
@IncludeModule(NonCachedModule.class)
class SecondNonCachedInjectorTest {

@Test
void test(long i) {
long expectedValue = NonCachedModule.FIRST_EXECUTED.get() ? 2 : 1;
assertEquals(expectedValue, i);
NonCachedModule.SECOND_EXECUTED.set(true);
}
}

@IncludeModule(OuterSharedClassModule.class)
@SharedInjectors
@Nested
class OuterSharedClass {

@IncludeModule(ExtraModule.class)
@Test
void testCase1(int value, @Named("extra") int extra) {
assertEquals(1, value);
assertEquals(ExtraModule.EXTRA2_EXECUTED.get() ? 2 : 1, extra);
ExtraModule.EXTRA1_EXECUTED.set(true);
}

@Test
@IncludeModule(ExtraModule.class)
void testCase2(int value, @Named("extra") int extra) {
assertEquals(1, value);
assertEquals(ExtraModule.EXTRA1_EXECUTED.get() ? 2 : 1, extra);
ExtraModule.EXTRA2_EXECUTED.set(true);
}
}

static class OuterSharedClassModule extends AbstractModule {

private final static AtomicInteger ATOMIC_INTEGER = new AtomicInteger(0);

@Override
protected void configure() {
int value = ATOMIC_INTEGER.incrementAndGet();
bind(int.class).toInstance(value);
assertEquals(1, value);
}
}

static class ExtraModule extends AbstractModule {

static final AtomicBoolean EXTRA1_EXECUTED = new AtomicBoolean(false);
static final AtomicBoolean EXTRA2_EXECUTED = new AtomicBoolean(false);
static final AtomicInteger INTEGER = new AtomicInteger(0);

@Override
protected void configure() {
bind(int.class).annotatedWith(Names.named("extra")).toInstance(INTEGER.incrementAndGet());
}
}

static class OuterClassModule extends AbstractModule {

@Override
protected void configure() {
bind(String.class).annotatedWith(Names.named("base")).toInstance("base");
}
}

static class InnerClassModule extends AbstractModule {

@Override
protected void configure() {
bind(String.class).annotatedWith(Names.named("inner")).toInstance("inner");
}
}

static class TestCaseModule extends AbstractModule {

@Override
protected void configure() {
bind(String.class).annotatedWith(Names.named("test")).toInstance("test");
}
}

static final class CachedModule extends AbstractModule {

static final AtomicLong ATOMIC_LONG = new AtomicLong(0);

@Override
protected void configure() {
bind(long.class).toInstance(ATOMIC_LONG.incrementAndGet());
}
}

static final class NonCachedModule extends AbstractModule {

static final AtomicLong ATOMIC_LONG = new AtomicLong(0);
//depend on which test executed first
static final AtomicBoolean FIRST_EXECUTED = new AtomicBoolean(false);
static final AtomicBoolean SECOND_EXECUTED = new AtomicBoolean(false);

@Override
protected void configure() {
bind(long.class).toInstance(ATOMIC_LONG.incrementAndGet());
}
}
}