Skip to content

Commit 3782bd5

Browse files
committed
Update to version 2.0. Instead of injectionMap, use @InjectSource. Allow for multiple fields to be annotated @Injectmocks. Add @PostConstruct support.
1 parent 5275579 commit 3782bd5

File tree

6 files changed

+166
-124
lines changed

6 files changed

+166
-124
lines changed

README.md

+39-20
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Inject Strings (or other objects) into your `@InjectMocks` targets [objects unde
44

55
## Problem
66

7-
Take this Spring Controller (or if you're using the far superior and modern CDI framework, imagine this is `@AppplicationScoped`)
7+
Take this Spring Controller (or if you're using the far superior and modern CDI framework, think `@AppplicationScoped` instead of `@Controller` and `@Inject` instead of `@Autowired`)
88

99
```
1010
@Controller
@@ -40,7 +40,7 @@ public class MyControllerTest {
4040
4141
public void testDoSomething() throws Exception {
4242
myController.doSomething();
43-
// results in NPE
43+
// results in NPE because myController.securityEnabled is null
4444
}
4545
}
4646
```
@@ -56,41 +56,60 @@ This JUnit5 extension allows you to arbitrarily set any field on your `@InjectMo
5656
import com.github.exabrial.junit5.injectmap.InjectionMap;
5757
import com.github.exabrial.junit5.injectmap.InjectMapExtension;
5858
59-
@TestInstance(Lifecycle.PER_METHOD)
60-
@ExtendWith({ MockitoExtension.class, InjectMapExtension.class })
59+
@TestInstance(Lifecycle.PER_CLASS)
60+
@ExtendWith({ MockitoExtension.class, InjectExtension.class })
6161
public class MyControllerTest {
6262
@InjectMocks
6363
private MyController myController;
6464
@Mock
6565
private Logger log;
6666
@Mock
6767
private Authenticator auther;
68-
@InjectionMap
69-
private Map<String, Object> injectionMap = new HashMap<>();
68+
@InjectionSource
69+
private Boolean securityEnabled;
7070
71-
@BeforeEach
72-
public void beforeEach() throws Exception {
73-
injectionMap.put("securityEnabled", Boolean.TRUE);
74-
}
75-
76-
@AfterEach
77-
public void afterEach() throws Exception {
78-
injectionMap.clear();
79-
}
80-
8171
public void testDoSomething_secEnabled() throws Exception {
72+
securityEnabled = Boolean.TRUE;
8273
myController.doSomething();
8374
// wahoo no NPE! Test the "if then" half of the branch
8475
}
8576
8677
public void testDoSomething_secDisabled() throws Exception {
87-
injectionMap.put("securityEnabled", Boolean.FALSE);
78+
securityEnabled = Boolean.FALSE;
8879
myController.doSomething();
8980
// wahoo no NPE! Test the "if else" half of branch
9081
}
9182
}
9283
```
9384

85+
## PostConstruct invocation
86+
87+
CDI and SpringFramework allow the use of `@PostConstruct`. This is like a constructor, except the method annotated will be invoked _after_ dependency injection is complete. This extension can be commanded to invoke the method annotated with `@PostConstruct` like so:
88+
89+
90+
```
91+
@ApplicationScoped
92+
public class MyController {
93+
@Inject
94+
private Logger log;
95+
96+
@PostConstruct
97+
private void postConstruct() {
98+
log.info("initializing myController...");
99+
... some initialization code
100+
}
101+
}
102+
```
103+
104+
```
105+
@InjectMocks
106+
@InvokePostConstruct
107+
private MyController myController;
108+
```
109+
110+
Ref: https://docs.oracle.com/javaee/7/api/javax/annotation/PostConstruct.html
111+
112+
94113
## License
95114

96115
All files are licensed Apache Source License 2.0. Please consider contributing any improvements you make back to the project.
@@ -103,19 +122,19 @@ Maven Coordinates:
103122
<dependency>
104123
<groupId>org.junit.jupiter</groupId>
105124
<artifactId>junit-jupiter-api</artifactId>
106-
<version>5.3.1</version>
125+
<version>5.6.2</version>
107126
<scope>test</scope>
108127
</dependency>
109128
<dependency>
110129
<groupId>org.mockito</groupId>
111130
<artifactId>mockito-core</artifactId>
112-
<version>2.23.4</version>
131+
<version>3.5.0</version>
113132
<scope>test</scope>
114133
</dependency>
115134
<dependency>
116135
<groupId>com.github.exabrial</groupId>
117136
<artifactId>mockito-object-injection</artifactId>
118-
<version>1.0.4</version>
137+
<version>2.0.0</version>
119138
<scope>test</scope>
120139
</dependency>
121140
```

pom.xml

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<modelVersion>4.0.0</modelVersion>
44
<groupId>com.github.exabrial</groupId>
55
<artifactId>mockito-object-injection</artifactId>
6-
<version>1.0.5-SNAPSHOT</version>
6+
<version>2.0.0-SNAPSHOT</version>
77
<packaging>jar</packaging>
88
<description>Mockito Object Injection</description>
99
<name>${project.artifactId}</name>
@@ -43,13 +43,13 @@
4343
<dependency>
4444
<groupId>org.junit.jupiter</groupId>
4545
<artifactId>junit-jupiter-api</artifactId>
46-
<version>5.3.1</version>
46+
<version>5.6.2</version>
4747
<scope>provided</scope>
4848
</dependency>
4949
<dependency>
5050
<groupId>org.mockito</groupId>
5151
<artifactId>mockito-core</artifactId>
52-
<version>2.23.0</version>
52+
<version>3.5.0</version>
5353
<scope>provided</scope>
5454
</dependency>
5555
<dependency>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package com.github.exabrial.junit5.injectmap;
2+
3+
import java.lang.reflect.Field;
4+
import java.lang.reflect.InvocationTargetException;
5+
import java.lang.reflect.Method;
6+
import java.lang.reflect.Modifier;
7+
import java.util.HashMap;
8+
import java.util.LinkedList;
9+
import java.util.List;
10+
import java.util.Map;
11+
12+
import javax.annotation.PostConstruct;
13+
14+
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
15+
import org.junit.jupiter.api.extension.ExtensionContext;
16+
import org.mockito.InjectMocks;
17+
18+
import javassist.util.proxy.MethodFilter;
19+
import javassist.util.proxy.MethodHandler;
20+
import javassist.util.proxy.Proxy;
21+
import javassist.util.proxy.ProxyFactory;
22+
23+
public class InjectExtension implements BeforeTestExecutionCallback {
24+
@Override
25+
public void beforeTestExecution(ExtensionContext context) throws Exception {
26+
Object testInstance = context.getTestInstance().get();
27+
if (testInstance != null) {
28+
final Map<String, Field> injectMap = new HashMap<>();
29+
for (Field testClassField : testInstance.getClass().getDeclaredFields()) {
30+
if (testClassField.getAnnotation(InjectMocks.class) != null) {
31+
testClassField.setAccessible(true);
32+
final Object injectionTarget = testClassField.get(testInstance);
33+
final ProxyFactory proxyFactory = new ProxyFactory();
34+
proxyFactory.setSuperclass(injectionTarget.getClass());
35+
proxyFactory.setFilter(createMethodFilter());
36+
final Class<?> proxyClass = proxyFactory.createClass();
37+
final Object proxy = proxyClass.newInstance();
38+
final Map<String, List<Field>> fieldMap = createFieldMap(injectionTarget.getClass());
39+
Method postConstructMethod;
40+
if (testClassField.getAnnotation(InvokePostConstruct.class) != null) {
41+
postConstructMethod = findPostConstructMethod(injectionTarget);
42+
} else {
43+
postConstructMethod = null;
44+
}
45+
final MethodHandler handler = createMethodHandler(injectMap, injectionTarget, fieldMap, testInstance, postConstructMethod);
46+
((Proxy) proxy).setHandler(handler);
47+
testClassField.set(testInstance, proxy);
48+
} else if (testClassField.getAnnotation(InjectionSource.class) != null) {
49+
injectMap.put(testClassField.getName(), testClassField);
50+
}
51+
}
52+
}
53+
}
54+
55+
private Method findPostConstructMethod(Object injectionTarget) {
56+
for (Method method : injectionTarget.getClass().getDeclaredMethods()) {
57+
if (method.isAnnotationPresent(PostConstruct.class)) {
58+
return method;
59+
}
60+
}
61+
throw new RuntimeException(
62+
"@InvokePostConstruct is delcared on:" + injectionTarget + " however no method annotated with @PostConstruct found");
63+
}
64+
65+
private Map<String, List<Field>> createFieldMap(Class<? extends Object> targetClass) {
66+
if (targetClass == Object.class) {
67+
return new HashMap<>();
68+
} else {
69+
Map<String, List<Field>> fieldMap = createFieldMap(targetClass.getSuperclass());
70+
for (Field field : targetClass.getDeclaredFields()) {
71+
List<Field> fieldList = fieldMap.get(field.getName());
72+
if (fieldList == null) {
73+
fieldList = new LinkedList<>();
74+
fieldMap.put(field.getName(), fieldList);
75+
}
76+
fieldList.add(field);
77+
}
78+
return fieldMap;
79+
}
80+
}
81+
82+
private MethodHandler createMethodHandler(final Map<String, Field> injectMap, final Object injectionTarget,
83+
final Map<String, List<Field>> fieldMap, final Object testInstance, final Method postConstructMethod) {
84+
return (proxy, invokedMethod, proceedMethod, args) -> {
85+
invokedMethod.setAccessible(true);
86+
for (String fieldName : injectMap.keySet()) {
87+
for (Field field : fieldMap.get(fieldName)) {
88+
field.setAccessible(true);
89+
field.set(injectionTarget, injectMap.get(fieldName).get(testInstance));
90+
}
91+
}
92+
if (postConstructMethod != null) {
93+
postConstructMethod.setAccessible(true);
94+
postConstructMethod.invoke(injectionTarget);
95+
}
96+
try {
97+
return invokedMethod.invoke(injectionTarget, args);
98+
} catch (InvocationTargetException itEx) {
99+
if (null != itEx.getCause()) {
100+
throw itEx.getCause();
101+
} else {
102+
throw itEx;
103+
}
104+
}
105+
};
106+
}
107+
108+
private MethodFilter createMethodFilter() {
109+
return method -> !Modifier.isPrivate(method.getModifiers());
110+
}
111+
}

src/main/java/com/github/exabrial/junit5/injectmap/InjectMapExtension.java

-100
This file was deleted.

src/main/java/com/github/exabrial/junit5/injectmap/InjectionMap.java src/main/java/com/github/exabrial/junit5/injectmap/InjectionSource.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@
88

99
@Retention(RUNTIME)
1010
@Target(FIELD)
11-
public @interface InjectionMap {
11+
public @interface InjectionSource {
1212
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.github.exabrial.junit5.injectmap;
2+
3+
import static java.lang.annotation.ElementType.FIELD;
4+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
5+
6+
import java.lang.annotation.Retention;
7+
import java.lang.annotation.Target;
8+
9+
@Retention(RUNTIME)
10+
@Target(FIELD)
11+
public @interface InvokePostConstruct {
12+
}

0 commit comments

Comments
 (0)