Skip to content

Commit 3bcb51d

Browse files
authored
Merge pull request #22 from dinject/feature/20-spy-support
Adding support for Mockito spy (#20) …
2 parents bf923f7 + 3b5f23e commit 3bcb51d

File tree

11 files changed

+492
-46
lines changed

11 files changed

+492
-46
lines changed

pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@
3131
<version>1.7.25</version>
3232
</dependency>
3333

34+
<dependency>
35+
<groupId>org.mockito</groupId>
36+
<artifactId>mockito-core</artifactId>
37+
<version>2.23.4</version>
38+
<scope>provided</scope>
39+
</dependency>
40+
3441
<!-- test dependencies -->
3542

3643
<dependency>

src/main/java/io/dinject/BootContext.java

Lines changed: 163 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import io.dinject.core.BeanContextFactory;
44
import io.dinject.core.Builder;
55
import io.dinject.core.BuilderFactory;
6+
import io.dinject.core.EnrichBean;
67
import io.dinject.core.SuppliedBean;
78
import org.slf4j.Logger;
89
import org.slf4j.LoggerFactory;
@@ -16,6 +17,7 @@
1617
import java.util.Map;
1718
import java.util.ServiceLoader;
1819
import java.util.Set;
20+
import java.util.function.Consumer;
1921

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

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

59+
private final List<EnrichBean> enrichBeans = new ArrayList<>();
60+
5761
private final Set<String> includeModules = new LinkedHashSet<>();
5862

5963
private boolean ignoreMissingModuleDependencies;
@@ -149,38 +153,41 @@ public BootContext withIgnoreMissingModuleDependencies() {
149153
}
150154

151155
/**
152-
* Supply a bean to the context that will be used instead of any similar bean in the context.
156+
* Supply a bean to the context that will be used instead of any
157+
* similar bean in the context.
153158
* <p>
154-
* This is typically expected to be used in tests and the bean supplied is typically a test double
155-
* or mock.
159+
* This is typically expected to be used in tests and the bean
160+
* supplied is typically a test double or mock.
156161
* </p>
157162
*
158163
* <pre>{@code
159164
*
160-
* @Test
161-
* public void someComponentTest() {
165+
* Pump pump = mock(Pump.class);
166+
* Grinder grinder = mock(Grinder.class);
162167
*
163-
* MyRedisApi mockRedis = mock(MyRedisApi.class);
164-
* MyDbApi mockDatabase = mock(MyDbApi.class);
168+
* try (BeanContext context = new BootContext()
169+
* .withBeans(pump, grinder)
170+
* .load()) {
165171
*
166-
* try (BeanContext context = new BootContext()
167-
* .withBeans(mockRedis, mockDatabase)
168-
* .load()) {
172+
* CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
173+
* coffeeMaker.makeIt();
169174
*
170-
* // built with test doubles injected ...
171-
* CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
172-
* coffeeMaker.makeIt();
175+
* Pump pump1 = context.getBean(Pump.class);
176+
* Grinder grinder1 = context.getBean(Grinder.class);
173177
*
174-
* assertThat(...
175-
* }
176-
* }
178+
* assertThat(pump1).isSameAs(pump);
179+
* assertThat(grinder1).isSameAs(grinder);
177180
*
181+
* verify(pump).pumpWater();
182+
* verify(grinder).grindBeans();
183+
* }
178184
*
179185
* }</pre>
180186
*
181187
* @param beans The bean used when injecting a dependency for this bean or the interface(s) it implements
182188
* @return This BootContext
183189
*/
190+
@SuppressWarnings("unchecked")
184191
public BootContext withBeans(Object... beans) {
185192
for (Object bean : beans) {
186193
suppliedBeans.add(new SuppliedBean(suppliedType(bean.getClass()), bean));
@@ -194,11 +201,148 @@ public BootContext withBeans(Object... beans) {
194201
* This is typically a test double often created by Mockito or similar.
195202
* </p>
196203
*
204+
* <pre>{@code
205+
*
206+
* try (BeanContext context = new BootContext()
207+
* .withBean(Pump.class, mock)
208+
* .load()) {
209+
*
210+
* Pump pump = context.getBean(Pump.class);
211+
* assertThat(pump).isSameAs(mock);
212+
*
213+
* // act
214+
* CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
215+
* coffeeMaker.makeIt();
216+
*
217+
* verify(pump).pumpSteam();
218+
* verify(pump).pumpWater();
219+
* }
220+
*
221+
* }</pre>
222+
*
197223
* @param type The dependency injection type this bean is target for
198224
* @param bean The supplied bean instance to use (typically a test mock)
199225
*/
200-
public BootContext withBean(Class<?> type, Object bean) {
201-
suppliedBeans.add(new SuppliedBean(type, bean));
226+
public <D> BootContext withBean(Class<D> type, D bean) {
227+
suppliedBeans.add(new SuppliedBean<>(type, bean));
228+
return this;
229+
}
230+
231+
/**
232+
* Use a mockito mock when injecting this bean type.
233+
*
234+
* <pre>{@code
235+
*
236+
* try (BeanContext context = new BootContext()
237+
* .withMock(Pump.class)
238+
* .withMock(Grinder.class, grinder -> {
239+
* // setup the mock
240+
* when(grinder.grindBeans()).thenReturn("stub response");
241+
* })
242+
* .load()) {
243+
*
244+
*
245+
* CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
246+
* coffeeMaker.makeIt();
247+
*
248+
* // this is a mockito mock
249+
* Grinder grinder = context.getBean(Grinder.class);
250+
* verify(grinder).grindBeans();
251+
* }
252+
*
253+
* }</pre>
254+
*/
255+
public <D> BootContext withMock(Class<D> type) {
256+
return withMock(type, null);
257+
}
258+
259+
/**
260+
* Use a mockito mock when injecting this bean type additionally
261+
* running setup on the mock instance.
262+
*
263+
* <pre>{@code
264+
*
265+
* try (BeanContext context = new BootContext()
266+
* .withMock(Pump.class)
267+
* .withMock(Grinder.class, grinder -> {
268+
*
269+
* // setup the mock
270+
* when(grinder.grindBeans()).thenReturn("stub response");
271+
* })
272+
* .load()) {
273+
*
274+
*
275+
* CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
276+
* coffeeMaker.makeIt();
277+
*
278+
* // this is a mockito mock
279+
* Grinder grinder = context.getBean(Grinder.class);
280+
* verify(grinder).grindBeans();
281+
* }
282+
*
283+
* }</pre>
284+
*/
285+
public <D> BootContext withMock(Class<D> type, Consumer<D> consumer) {
286+
suppliedBeans.add(new SuppliedBean<>(type, null, consumer));
287+
return this;
288+
}
289+
290+
/**
291+
* Use a mockito spy when injecting this bean type.
292+
*
293+
* <pre>{@code
294+
*
295+
* try (BeanContext context = new BootContext()
296+
* .withSpy(Pump.class)
297+
* .load()) {
298+
*
299+
* // setup spy here ...
300+
* Pump pump = context.getBean(Pump.class);
301+
* doNothing().when(pump).pumpSteam();
302+
*
303+
* // act
304+
* CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
305+
* coffeeMaker.makeIt();
306+
*
307+
* verify(pump).pumpWater();
308+
* verify(pump).pumpSteam();
309+
* }
310+
*
311+
* }</pre>
312+
*/
313+
public <D> BootContext withSpy(Class<D> type) {
314+
return withSpy(type, null);
315+
}
316+
317+
/**
318+
* Use a mockito spy when injecting this bean type additionally
319+
* running setup on the spy instance.
320+
*
321+
* <pre>{@code
322+
*
323+
* try (BeanContext context = new BootContext()
324+
* .withSpy(Pump.class, pump -> {
325+
* // setup the spy
326+
* doNothing().when(pump).pumpWater();
327+
* })
328+
* .load()) {
329+
*
330+
* // or setup here ...
331+
* Pump pump = context.getBean(Pump.class);
332+
* doNothing().when(pump).pumpSteam();
333+
*
334+
* // act
335+
* CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
336+
* coffeeMaker.makeIt();
337+
*
338+
* verify(pump).pumpWater();
339+
* verify(pump).pumpSteam();
340+
* }
341+
*
342+
* }</pre>
343+
*/
344+
public <D> BootContext withSpy(Class<D> type, Consumer<D> consumer) {
345+
enrichBeans.add(new EnrichBean<>(type, consumer));
202346
return this;
203347
}
204348

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

217-
Builder rootBuilder = BuilderFactory.newRootBuilder(suppliedBeans);
361+
Builder rootBuilder = BuilderFactory.newRootBuilder(suppliedBeans, enrichBeans);
218362

219363
for (BeanContextFactory factory : factoryOrder.factories()) {
220364
rootBuilder.addChild(factory.createContext(rootBuilder));

src/main/java/io/dinject/core/Builder.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,4 +125,13 @@ public interface Builder {
125125
*/
126126
BeanContext build();
127127

128+
/**
129+
* Return a potentially enriched bean for registration into the context.
130+
* Typically for use with mockito spy.
131+
*
132+
* @param bean The bean with dependencies injected
133+
* @param types The types this bean registers for
134+
* @return Either the bean or the enriched bean to register into the context.
135+
*/
136+
Object enrich(Object bean, Class<?>[] types);
128137
}

src/main/java/io/dinject/core/BuilderFactory.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,15 @@ public class BuilderFactory {
1313
* Create the root level Builder.
1414
*
1515
* @param suppliedBeans The list of beans (typically test doubles) supplied when building the context.
16+
* @param enrichBeans The list of classes we want to have with mockito spy enhancement
1617
*/
17-
public static Builder newRootBuilder(List<SuppliedBean> suppliedBeans) {
18-
return new DBuilder(suppliedBeans);
18+
public static Builder newRootBuilder(List<SuppliedBean> suppliedBeans, List<EnrichBean> enrichBeans) {
19+
20+
if (suppliedBeans.isEmpty() && enrichBeans.isEmpty()) {
21+
// simple case, no mocks or spies
22+
return new DBuilder();
23+
}
24+
return new DBuilderExtn(suppliedBeans, enrichBeans);
1925
}
2026

2127
/**

src/main/java/io/dinject/core/DBuilder.java

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,7 @@ class DBuilder implements Builder {
2929
/**
3030
* The beans created and added to the context during building.
3131
*/
32-
private final DBeanMap beanMap = new DBeanMap();
33-
34-
/**
35-
* Supplied beans (test doubles) given to the context prior to building.
36-
*/
37-
private final boolean hasSuppliedBeans;
32+
final DBeanMap beanMap = new DBeanMap();
3833

3934
/**
4035
* The context/module name.
@@ -58,7 +53,7 @@ class DBuilder implements Builder {
5853
*/
5954
private Class<?> injectTarget;
6055

61-
private Builder parent;
56+
Builder parent;
6257

6358
/**
6459
* Create a named context for non-root builders.
@@ -67,20 +62,15 @@ class DBuilder implements Builder {
6762
this.name = name;
6863
this.provides = provides;
6964
this.dependsOn = dependsOn;
70-
this.hasSuppliedBeans = false;
7165
}
7266

7367
/**
74-
* Create for the root builder with supplied beans (test doubles).
68+
* Create for the root builder.
7569
*/
76-
DBuilder(List<SuppliedBean> suppliedBeans) {
70+
DBuilder() {
7771
this.name = null;
7872
this.provides = null;
7973
this.dependsOn = null;
80-
this.hasSuppliedBeans = (suppliedBeans != null && !suppliedBeans.isEmpty());
81-
if (hasSuppliedBeans) {
82-
beanMap.add(suppliedBeans);
83-
}
8474
}
8575

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

10696
@Override
10797
public boolean isAddBeanFor(Class<?> addForType, Class<?> injectTarget) {
108-
if (hasSuppliedBeans) {
109-
return !beanMap.isSupplied(addForType.getName());
110-
}
11198
if (parent == null) {
11299
return true;
113100
}
@@ -164,9 +151,22 @@ public void addChild(BeanContext child) {
164151

165152
@Override
166153
public void register(Object bean, String name, Class<?>... types) {
154+
if (parent != null) {
155+
// enrichment only exist on top level builder
156+
bean = parent.enrich(bean, types);
157+
}
167158
beanMap.register(bean, name, types);
168159
}
169160

161+
/**
162+
* Return the bean to register potentially with spy enhancement.
163+
*/
164+
@Override
165+
public Object enrich(Object bean, Class<?>[] types) {
166+
// only enriched by DBuilderExtn
167+
return bean;
168+
}
169+
170170
@Override
171171
public void registerPrimary(Object bean, String name, Class<?>... types) {
172172
beanMap.registerPrimary(bean, name, types);

0 commit comments

Comments
 (0)