Skip to content

Commit

Permalink
Merge pull request #22 from dinject/feature/20-spy-support
Browse files Browse the repository at this point in the history
Adding support for Mockito spy  (#20) …
  • Loading branch information
rbygrave authored May 30, 2019
2 parents bf923f7 + 3b5f23e commit 3bcb51d
Show file tree
Hide file tree
Showing 11 changed files with 492 additions and 46 deletions.
7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@
<version>1.7.25</version>
</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.23.4</version>
<scope>provided</scope>
</dependency>

<!-- test dependencies -->

<dependency>
Expand Down
182 changes: 163 additions & 19 deletions src/main/java/io/dinject/BootContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.dinject.core.BeanContextFactory;
import io.dinject.core.Builder;
import io.dinject.core.BuilderFactory;
import io.dinject.core.EnrichBean;
import io.dinject.core.SuppliedBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -16,6 +17,7 @@
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.function.Consumer;

/**
* Boot and create a bean context with options for shutdown hook and supplying test doubles.
Expand Down Expand Up @@ -54,6 +56,8 @@ public class BootContext {

private final List<SuppliedBean> suppliedBeans = new ArrayList<>();

private final List<EnrichBean> enrichBeans = new ArrayList<>();

private final Set<String> includeModules = new LinkedHashSet<>();

private boolean ignoreMissingModuleDependencies;
Expand Down Expand Up @@ -149,38 +153,41 @@ public BootContext withIgnoreMissingModuleDependencies() {
}

/**
* Supply a bean to the context that will be used instead of any similar bean in the context.
* Supply a bean to the context that will be used instead of any
* similar bean in the context.
* <p>
* This is typically expected to be used in tests and the bean supplied is typically a test double
* or mock.
* This is typically expected to be used in tests and the bean
* supplied is typically a test double or mock.
* </p>
*
* <pre>{@code
*
* @Test
* public void someComponentTest() {
* Pump pump = mock(Pump.class);
* Grinder grinder = mock(Grinder.class);
*
* MyRedisApi mockRedis = mock(MyRedisApi.class);
* MyDbApi mockDatabase = mock(MyDbApi.class);
* try (BeanContext context = new BootContext()
* .withBeans(pump, grinder)
* .load()) {
*
* try (BeanContext context = new BootContext()
* .withBeans(mockRedis, mockDatabase)
* .load()) {
* CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
* coffeeMaker.makeIt();
*
* // built with test doubles injected ...
* CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
* coffeeMaker.makeIt();
* Pump pump1 = context.getBean(Pump.class);
* Grinder grinder1 = context.getBean(Grinder.class);
*
* assertThat(...
* }
* }
* assertThat(pump1).isSameAs(pump);
* assertThat(grinder1).isSameAs(grinder);
*
* verify(pump).pumpWater();
* verify(grinder).grindBeans();
* }
*
* }</pre>
*
* @param beans The bean used when injecting a dependency for this bean or the interface(s) it implements
* @return This BootContext
*/
@SuppressWarnings("unchecked")
public BootContext withBeans(Object... beans) {
for (Object bean : beans) {
suppliedBeans.add(new SuppliedBean(suppliedType(bean.getClass()), bean));
Expand All @@ -194,11 +201,148 @@ public BootContext withBeans(Object... beans) {
* This is typically a test double often created by Mockito or similar.
* </p>
*
* <pre>{@code
*
* try (BeanContext context = new BootContext()
* .withBean(Pump.class, mock)
* .load()) {
*
* Pump pump = context.getBean(Pump.class);
* assertThat(pump).isSameAs(mock);
*
* // act
* CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
* coffeeMaker.makeIt();
*
* verify(pump).pumpSteam();
* verify(pump).pumpWater();
* }
*
* }</pre>
*
* @param type The dependency injection type this bean is target for
* @param bean The supplied bean instance to use (typically a test mock)
*/
public BootContext withBean(Class<?> type, Object bean) {
suppliedBeans.add(new SuppliedBean(type, bean));
public <D> BootContext withBean(Class<D> type, D bean) {
suppliedBeans.add(new SuppliedBean<>(type, bean));
return this;
}

/**
* Use a mockito mock when injecting this bean type.
*
* <pre>{@code
*
* try (BeanContext context = new BootContext()
* .withMock(Pump.class)
* .withMock(Grinder.class, grinder -> {
* // setup the mock
* when(grinder.grindBeans()).thenReturn("stub response");
* })
* .load()) {
*
*
* CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
* coffeeMaker.makeIt();
*
* // this is a mockito mock
* Grinder grinder = context.getBean(Grinder.class);
* verify(grinder).grindBeans();
* }
*
* }</pre>
*/
public <D> BootContext withMock(Class<D> type) {
return withMock(type, null);
}

/**
* Use a mockito mock when injecting this bean type additionally
* running setup on the mock instance.
*
* <pre>{@code
*
* try (BeanContext context = new BootContext()
* .withMock(Pump.class)
* .withMock(Grinder.class, grinder -> {
*
* // setup the mock
* when(grinder.grindBeans()).thenReturn("stub response");
* })
* .load()) {
*
*
* CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
* coffeeMaker.makeIt();
*
* // this is a mockito mock
* Grinder grinder = context.getBean(Grinder.class);
* verify(grinder).grindBeans();
* }
*
* }</pre>
*/
public <D> BootContext withMock(Class<D> type, Consumer<D> consumer) {
suppliedBeans.add(new SuppliedBean<>(type, null, consumer));
return this;
}

/**
* Use a mockito spy when injecting this bean type.
*
* <pre>{@code
*
* try (BeanContext context = new BootContext()
* .withSpy(Pump.class)
* .load()) {
*
* // setup spy here ...
* Pump pump = context.getBean(Pump.class);
* doNothing().when(pump).pumpSteam();
*
* // act
* CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
* coffeeMaker.makeIt();
*
* verify(pump).pumpWater();
* verify(pump).pumpSteam();
* }
*
* }</pre>
*/
public <D> BootContext withSpy(Class<D> type) {
return withSpy(type, null);
}

/**
* Use a mockito spy when injecting this bean type additionally
* running setup on the spy instance.
*
* <pre>{@code
*
* try (BeanContext context = new BootContext()
* .withSpy(Pump.class, pump -> {
* // setup the spy
* doNothing().when(pump).pumpWater();
* })
* .load()) {
*
* // or setup here ...
* Pump pump = context.getBean(Pump.class);
* doNothing().when(pump).pumpSteam();
*
* // act
* CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
* coffeeMaker.makeIt();
*
* verify(pump).pumpWater();
* verify(pump).pumpSteam();
* }
*
* }</pre>
*/
public <D> BootContext withSpy(Class<D> type, Consumer<D> consumer) {
enrichBeans.add(new EnrichBean<>(type, consumer));
return this;
}

Expand All @@ -214,7 +358,7 @@ public BeanContext load() {
Set<String> moduleNames = factoryOrder.orderFactories();
log.debug("building context with modules {}", moduleNames);

Builder rootBuilder = BuilderFactory.newRootBuilder(suppliedBeans);
Builder rootBuilder = BuilderFactory.newRootBuilder(suppliedBeans, enrichBeans);

for (BeanContextFactory factory : factoryOrder.factories()) {
rootBuilder.addChild(factory.createContext(rootBuilder));
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/io/dinject/core/Builder.java
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,13 @@ public interface Builder {
*/
BeanContext build();

/**
* Return a potentially enriched bean for registration into the context.
* Typically for use with mockito spy.
*
* @param bean The bean with dependencies injected
* @param types The types this bean registers for
* @return Either the bean or the enriched bean to register into the context.
*/
Object enrich(Object bean, Class<?>[] types);
}
10 changes: 8 additions & 2 deletions src/main/java/io/dinject/core/BuilderFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,15 @@ public class BuilderFactory {
* Create the root level Builder.
*
* @param suppliedBeans The list of beans (typically test doubles) supplied when building the context.
* @param enrichBeans The list of classes we want to have with mockito spy enhancement
*/
public static Builder newRootBuilder(List<SuppliedBean> suppliedBeans) {
return new DBuilder(suppliedBeans);
public static Builder newRootBuilder(List<SuppliedBean> suppliedBeans, List<EnrichBean> enrichBeans) {

if (suppliedBeans.isEmpty() && enrichBeans.isEmpty()) {
// simple case, no mocks or spies
return new DBuilder();
}
return new DBuilderExtn(suppliedBeans, enrichBeans);
}

/**
Expand Down
34 changes: 17 additions & 17 deletions src/main/java/io/dinject/core/DBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,7 @@ class DBuilder implements Builder {
/**
* The beans created and added to the context during building.
*/
private final DBeanMap beanMap = new DBeanMap();

/**
* Supplied beans (test doubles) given to the context prior to building.
*/
private final boolean hasSuppliedBeans;
final DBeanMap beanMap = new DBeanMap();

/**
* The context/module name.
Expand All @@ -58,7 +53,7 @@ class DBuilder implements Builder {
*/
private Class<?> injectTarget;

private Builder parent;
Builder parent;

/**
* Create a named context for non-root builders.
Expand All @@ -67,20 +62,15 @@ class DBuilder implements Builder {
this.name = name;
this.provides = provides;
this.dependsOn = dependsOn;
this.hasSuppliedBeans = false;
}

/**
* Create for the root builder with supplied beans (test doubles).
* Create for the root builder.
*/
DBuilder(List<SuppliedBean> suppliedBeans) {
DBuilder() {
this.name = null;
this.provides = null;
this.dependsOn = null;
this.hasSuppliedBeans = (suppliedBeans != null && !suppliedBeans.isEmpty());
if (hasSuppliedBeans) {
beanMap.add(suppliedBeans);
}
}

@Override
Expand All @@ -105,9 +95,6 @@ public void setParent(Builder parent) {

@Override
public boolean isAddBeanFor(Class<?> addForType, Class<?> injectTarget) {
if (hasSuppliedBeans) {
return !beanMap.isSupplied(addForType.getName());
}
if (parent == null) {
return true;
}
Expand Down Expand Up @@ -164,9 +151,22 @@ public void addChild(BeanContext child) {

@Override
public void register(Object bean, String name, Class<?>... types) {
if (parent != null) {
// enrichment only exist on top level builder
bean = parent.enrich(bean, types);
}
beanMap.register(bean, name, types);
}

/**
* Return the bean to register potentially with spy enhancement.
*/
@Override
public Object enrich(Object bean, Class<?>[] types) {
// only enriched by DBuilderExtn
return bean;
}

@Override
public void registerPrimary(Object bean, String name, Class<?>... types) {
beanMap.registerPrimary(bean, name, types);
Expand Down
Loading

0 comments on commit 3bcb51d

Please sign in to comment.