Skip to content

Commit 1b7d87a

Browse files
committed
#20 - Swap/Change SystemContext objects with mocks
1 parent 50dcd0a commit 1b7d87a

File tree

8 files changed

+137
-83
lines changed

8 files changed

+137
-83
lines changed

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

Lines changed: 32 additions & 2 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.SuppliedBean;
67
import org.slf4j.Logger;
78
import org.slf4j.LoggerFactory;
89

@@ -51,7 +52,7 @@ public class BootContext {
5152

5253
private boolean shutdownHook = true;
5354

54-
private final List<Object> suppliedBeans = new ArrayList<>();
55+
private final List<SuppliedBean> suppliedBeans = new ArrayList<>();
5556

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

@@ -181,7 +182,23 @@ public BootContext withIgnoreMissingModuleDependencies() {
181182
* @return This BootContext
182183
*/
183184
public BootContext withBeans(Object... beans) {
184-
suppliedBeans.addAll(Arrays.asList(beans));
185+
for (Object bean : beans) {
186+
suppliedBeans.add(new SuppliedBean(suppliedType(bean.getClass()), bean));
187+
}
188+
return this;
189+
}
190+
191+
/**
192+
* Add a supplied bean instance with the given injection type.
193+
* <p>
194+
* This is typically a test double often created by Mockito or similar.
195+
* </p>
196+
*
197+
* @param type The dependency injection type this bean is target for
198+
* @param bean The supplied bean instance to use (typically a test mock)
199+
*/
200+
public BootContext withBean(Class<?> type, Object bean) {
201+
suppliedBeans.add(new SuppliedBean(type, bean));
185202
return this;
186203
}
187204

@@ -214,6 +231,19 @@ public BeanContext load() {
214231
return beanContext;
215232
}
216233

234+
/**
235+
* Return the type that we map the supplied bean to.
236+
*/
237+
private Class<?> suppliedType(Class<?> suppliedClass) {
238+
Class<?> suppliedSuper = suppliedClass.getSuperclass();
239+
if (Object.class.equals(suppliedSuper)) {
240+
return suppliedClass;
241+
} else {
242+
// prefer to use the super type of the supplied bean (test double)
243+
return suppliedSuper;
244+
}
245+
}
246+
217247
/**
218248
* Internal shutdown hook.
219249
*/

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public class BuilderFactory {
1414
*
1515
* @param suppliedBeans The list of beans (typically test doubles) supplied when building the context.
1616
*/
17-
public static Builder newRootBuilder(List<Object> suppliedBeans) {
17+
public static Builder newRootBuilder(List<SuppliedBean> suppliedBeans) {
1818
return new DBuilder(suppliedBeans);
1919
}
2020

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

Lines changed: 20 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -18,60 +18,36 @@
1818
*/
1919
class DBeanMap {
2020

21-
private final Map<String, DContextEntry> beans;
21+
private final Map<String, DContextEntry> beans = new LinkedHashMap<>();
2222

2323
/**
24-
* Create for root builder with supplied beans (usually test doubles).
25-
* <p>
26-
* Supplied beans are typically test doubles (used in testing) that replace the normally
27-
* injected bean. For example, a stub for a database API.
28-
* </p>
24+
* Create for context builder.
2925
*/
30-
DBeanMap(List<Object> suppliedBeans) {
31-
if (suppliedBeans.isEmpty()) {
32-
beans = null;
33-
} else {
34-
beans = new LinkedHashMap<>();
35-
for (Object suppliedBean : suppliedBeans) {
36-
addSuppliedBean(suppliedBean);
37-
}
38-
}
26+
DBeanMap() {
3927
}
4028

4129
/**
42-
* Create for context builder.
30+
* Add test double supplied beans.
4331
*/
44-
DBeanMap() {
45-
beans = new LinkedHashMap<>();
32+
void add(List<SuppliedBean> suppliedBeans) {
33+
for (SuppliedBean suppliedBean : suppliedBeans) {
34+
addSuppliedBean(suppliedBean);
35+
}
4636
}
4737

48-
private void addSuppliedBean(Object bean) {
38+
private void addSuppliedBean(SuppliedBean supplied) {
4939

50-
Class<?> suppliedClass = bean.getClass();
51-
Class<?> suppliedType = suppliedType(suppliedClass);
52-
Named annotation = suppliedClass.getAnnotation(Named.class);
40+
Class<?> suppliedType = supplied.getType();
41+
Named annotation = suppliedType.getAnnotation(Named.class);
5342
String name = (annotation == null) ? null : annotation.value();
5443

55-
DContextEntryBean entryBean = DContextEntryBean.of(bean, name, SUPPLIED);
44+
DContextEntryBean entryBean = DContextEntryBean.of(supplied.getBean(), name, SUPPLIED);
5645
beans.computeIfAbsent(suppliedType.getCanonicalName(), s -> new DContextEntry()).add(entryBean);
57-
for (Class<?> anInterface : suppliedClass.getInterfaces()) {
46+
for (Class<?> anInterface : suppliedType.getInterfaces()) {
5847
beans.computeIfAbsent(anInterface.getCanonicalName(), s -> new DContextEntry()).add(entryBean);
5948
}
6049
}
6150

62-
/**
63-
* Return the type that we map the supplied bean to.
64-
*/
65-
private Class<?> suppliedType(Class<?> suppliedClass) {
66-
Class<?> suppliedSuper = suppliedClass.getSuperclass();
67-
if (Object.class.equals(suppliedSuper)) {
68-
return suppliedClass;
69-
} else {
70-
// prefer to use the super type of the supplied bean (test double)
71-
return suppliedSuper;
72-
}
73-
}
74-
7551
void registerPrimary(Object bean, String name, Class<?>... types) {
7652
registerWith(PRIMARY, bean, name, types);
7753
}
@@ -102,10 +78,6 @@ void registerWith(int flag, Object bean, String name, Class<?>... types) {
10278
@SuppressWarnings("unchecked")
10379
<T> T getBean(Class<T> type, String name) {
10480

105-
if (beans == null) {
106-
// no beans in the root suppliedBeanMap
107-
return null;
108-
}
10981
DContextEntry entry = beans.get(type.getCanonicalName());
11082
if (entry != null) {
11183
T bean = (T) entry.get(name);
@@ -118,10 +90,6 @@ <T> T getBean(Class<T> type, String name) {
11890

11991
<T> BeanEntry<T> candidate(Class<T> type, String name) {
12092

121-
if (beans == null) {
122-
// no beans in the root suppliedBeanMap
123-
return null;
124-
}
12593
DContextEntry entry = beans.get(type.getCanonicalName());
12694
if (entry != null) {
12795
return entry.candidate(name);
@@ -134,25 +102,17 @@ <T> BeanEntry<T> candidate(Class<T> type, String name) {
134102
*/
135103
@SuppressWarnings("unchecked")
136104
void addAll(Class type, List list) {
137-
if (beans != null) {
138-
DContextEntry entry = beans.get(type.getCanonicalName());
139-
if (entry != null) {
140-
entry.addAll(list);
141-
}
105+
DContextEntry entry = beans.get(type.getCanonicalName());
106+
if (entry != null) {
107+
entry.addAll(list);
142108
}
143109
}
144110

145111
/**
146-
* Return true if the bean for the given type should be created.
147-
* <p>
148-
* Return false indicates the type has a supplied (test double) instance that should be used instead and that
149-
* means context building will skip creating this bean.
150-
* </p>
112+
* Return true if there is a supplied bean for this type.
151113
*/
152-
boolean isAddBeanFor(String type) {
153-
if (beans == null) {
154-
return true;
155-
}
156-
return !beans.containsKey(type);
114+
boolean isSupplied(String type) {
115+
DContextEntry entry = beans.get(type);
116+
return entry != null && entry.isSupplied();
157117
}
158118
}

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

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class DBuilder implements Builder {
3434
/**
3535
* Supplied beans (test doubles) given to the context prior to building.
3636
*/
37-
private final DBeanMap suppliedBeanMap;
37+
private final boolean hasSuppliedBeans;
3838

3939
/**
4040
* The context/module name.
@@ -67,17 +67,20 @@ class DBuilder implements Builder {
6767
this.name = name;
6868
this.provides = provides;
6969
this.dependsOn = dependsOn;
70-
this.suppliedBeanMap = null;
70+
this.hasSuppliedBeans = false;
7171
}
7272

7373
/**
7474
* Create for the root builder with supplied beans (test doubles).
7575
*/
76-
DBuilder(List<Object> suppliedBeans) {
76+
DBuilder(List<SuppliedBean> suppliedBeans) {
7777
this.name = null;
7878
this.provides = null;
7979
this.dependsOn = null;
80-
this.suppliedBeanMap = new DBeanMap(suppliedBeans);
80+
this.hasSuppliedBeans = (suppliedBeans != null && !suppliedBeans.isEmpty());
81+
if (hasSuppliedBeans) {
82+
beanMap.add(suppliedBeans);
83+
}
8184
}
8285

8386
@Override
@@ -102,8 +105,11 @@ public void setParent(Builder parent) {
102105

103106
@Override
104107
public boolean isAddBeanFor(Class<?> addForType, Class<?> injectTarget) {
105-
if (suppliedBeanMap != null) {
106-
return suppliedBeanMap.isAddBeanFor(addForType.getName());
108+
if (hasSuppliedBeans) {
109+
return !beanMap.isSupplied(addForType.getName());
110+
}
111+
if (parent == null) {
112+
return true;
107113
}
108114
this.injectTarget = injectTarget;
109115
return parent.isAddBeanFor(addForType);
@@ -119,13 +125,6 @@ public boolean isAddBeanFor(Class<?> injectTarget) {
119125
public <T> List<T> getList(Class<T> interfaceType) {
120126

121127
List list = new ArrayList<>();
122-
if (suppliedBeanMap != null) {
123-
suppliedBeanMap.addAll(interfaceType, list);
124-
if (!list.isEmpty()) {
125-
// not sure if this is correct, supplied so skip
126-
return (List<T>) list;
127-
}
128-
}
129128

130129
beanMap.addAll(interfaceType, list);
131130
for (BeanContext childContext : children.values()) {
@@ -140,13 +139,6 @@ public <T> List<T> getList(Class<T> interfaceType) {
140139
@Override
141140
public <T> BeanEntry<T> candidate(Class<T> cls, String name) {
142141

143-
if (suppliedBeanMap != null) {
144-
BeanEntry<T> entry = suppliedBeanMap.candidate(cls, name);
145-
if (entry != null) {
146-
return entry;
147-
}
148-
}
149-
150142
DBeanContext.EntrySort<T> entrySort = new DBeanContext.EntrySort<>();
151143
entrySort.add(beanMap.candidate(cls, name));
152144
for (BeanContext childContext : children.values()) {

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,18 @@ void add(DContextEntryBean entryBean) {
5959
entries.add(entryBean);
6060
}
6161

62+
/**
63+
* Return true if a supplied bean is one of the entries.
64+
*/
65+
boolean isSupplied() {
66+
for (DContextEntryBean entry : entries) {
67+
if (entry.isSupplied()) {
68+
return true;
69+
}
70+
}
71+
return false;
72+
}
73+
6274
static class EntryMatcher {
6375

6476
private final String name;

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ boolean isSecondary() {
6666
return flag == BeanEntry.SECONDARY;
6767
}
6868

69+
boolean isSupplied() {
70+
return flag == BeanEntry.SUPPLIED;
71+
}
72+
6973
@SuppressWarnings("unchecked")
7074
BeanEntry getBeanEntry() {
7175
return new BeanEntry(flag, getBean(), name);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package io.dinject.core;
2+
3+
/**
4+
* Holds beans supplied to the dependency injection.
5+
* <p>
6+
* These are typically test doubles or mock instances that we supply in testing.
7+
* When we supply bean instances they take precedence over other beans that
8+
* would normally be injected.
9+
* </p>
10+
*/
11+
public class SuppliedBean {
12+
13+
private final Class<?> type;
14+
15+
private final Object bean;
16+
17+
/**
18+
* Create with a given target type and bean instance.
19+
*/
20+
public SuppliedBean(Class<?> type, Object bean) {
21+
this.type = type;
22+
this.bean = bean;
23+
}
24+
25+
/**
26+
* Return the dependency injection target type.
27+
*/
28+
public Class<?> getType() {
29+
return type;
30+
}
31+
32+
/**
33+
* Return the bean instance to use (often a test double or mock).
34+
*/
35+
public Object getBean() {
36+
return bean;
37+
}
38+
}

src/test/java/org/example/coffee/BootContextAddTest.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import io.dinject.BeanContext;
44
import io.dinject.BootContext;
55
import org.junit.Test;
6+
import org.mockito.Mockito;
67

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

@@ -61,6 +62,23 @@ public void withBean_expect_testDoublePumpUsed() {
6162
}
6263
}
6364

65+
@Test
66+
public void withMockitoMock_expect_mockUsed() {
67+
68+
Pump mock = Mockito.mock(Pump.class);
69+
70+
try (BeanContext context = new BootContext()
71+
.withBean(Pump.class, mock)
72+
.load()) {
73+
74+
Pump pump = context.getBean(Pump.class);
75+
assertThat(pump).isSameAs(mock);
76+
77+
CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
78+
assertThat(coffeeMaker).isNotNull();
79+
}
80+
}
81+
6482
/**
6583
* Our test double that we want to wire.
6684
*/

0 commit comments

Comments
 (0)