Skip to content

Commit 441eb4d

Browse files
authored
Add support for Google TestParameterInjector (#137)
1 parent ee47f98 commit 441eb4d

File tree

13 files changed

+288
-16
lines changed

13 files changed

+288
-16
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* [Parameterized](https://github.com/junit-team/junit4/blob/main/src/main/java/org/junit/runners/Parameterized.java)
1313
* [Theories](https://github.com/junit-team/junit4/blob/main/src/main/java/org/junit/experimental/theories/Theories.java)
1414
* [JUnitParamsRunner](https://github.com/Pragmatists/JUnitParams/blob/master/src/main/java/junitparams/JUnitParamsRunner.java)
15+
* [TestParameterInjector](https://github.com/google/TestParameterInjector/blob/main/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java)
1516
* [PowerMockRunner](https://github.com/powermock/powermock/blob/release/2.x/powermock-modules/powermock-module-junit4/src/main/java/org/powermock/modules/junit4/PowerMockRunner.java) (requires delegation)
1617

1718
The native implementation of **PowerMockRunner** uses a deprecated JUnit runner model that **JUnit Foundation** doesn't support. You need to delegate test execution to the standard **BlockJUnit4ClassRunner** (or subclasses thereof) to enable reporting of test lifecycle events. This is specified via the [@PowerMockRunnerDelegate](https://github.com/powermock/powermock/blob/release/2.x/powermock-modules/powermock-module-junit4/src/main/java/org/powermock/modules/junit4/PowerMockRunnerDelegate.java) annotation, as shown below:

pom.xml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
<logback.version>1.2.11</logback.version>
3737
<junitparams.version>1.1.1</junitparams.version>
3838
<powermock.version>2.0.9</powermock.version>
39+
<paraminjector.version>1.18</paraminjector.version>
3940
<compiler-plugin.version>3.10.1</compiler-plugin.version>
4041
<surefire-plugin.version>3.0.0-M7</surefire-plugin.version>
4142
<source-plugin.version>3.2.1</source-plugin.version>
@@ -117,6 +118,11 @@
117118
<artifactId>powermock-api-mockito2</artifactId>
118119
<version>${powermock.version}</version>
119120
</dependency>
121+
<dependency>
122+
<groupId>com.google.testparameterinjector</groupId>
123+
<artifactId>test-parameter-injector</artifactId>
124+
<version>${paraminjector.version}</version>
125+
</dependency>
120126
</dependencies>
121127
</dependencyManagement>
122128

@@ -167,6 +173,11 @@
167173
<artifactId>powermock-api-mockito2</artifactId>
168174
<scope>test</scope>
169175
</dependency>
176+
<dependency>
177+
<groupId>com.google.testparameterinjector</groupId>
178+
<artifactId>test-parameter-injector</artifactId>
179+
<scope>test</scope>
180+
</dependency>
170181
</dependencies>
171182

172183
<build>
@@ -219,6 +230,11 @@
219230
<plugin>
220231
<groupId>org.apache.maven.plugins</groupId>
221232
<artifactId>maven-compiler-plugin</artifactId>
233+
<configuration>
234+
<compilerArgs>
235+
<arg>-parameters</arg>
236+
</compilerArgs>
237+
</configuration>
222238
</plugin>
223239
<plugin>
224240
<groupId>org.apache.maven.plugins</groupId>

src/main/java/com/nordstrom/automation/junit/ArtifactCollector.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,12 +141,21 @@ private String getArtifactBaseName() {
141141
int hashcode = getParameters().hashCode();
142142
if (hashcode != 0) {
143143
String hashStr = String.format("%08X", hashcode);
144-
return getDescription().getMethodName() + "-" + hashStr;
144+
return getSanitizedName() + "-" + hashStr;
145145
} else {
146-
return getDescription().getMethodName();
146+
return getSanitizedName();
147147
}
148148
}
149149

150+
/**
151+
* Get the target method name, replacing Windows file name reserved characters with '_'.
152+
*
153+
* @return sanitized target method name
154+
*/
155+
private String getSanitizedName() {
156+
return getDescription().getMethodName().replaceAll("[\\/:*?\"<>|]", "_");
157+
}
158+
150159
/**
151160
* Record the path at which the specified artifact was store in the indicated test result.
152161
*

src/main/java/com/nordstrom/automation/junit/AtomicTest.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
public class AtomicTest {
3131
private final Object runner;
3232
private final Description description;
33-
private final FrameworkMethod identity;
33+
private FrameworkMethod identity;
3434
private final List<FrameworkMethod> particles;
3535
private Throwable thrown;
3636

@@ -61,6 +61,14 @@ public Object getRunner() {
6161
public Description getDescription() {
6262
return description;
6363
}
64+
65+
/**
66+
* Set the "identity" method for this atomic test - the core {@link Test &#64;Test} method.
67+
*/
68+
void setIdentity(FrameworkMethod method) {
69+
identity = method;
70+
particles.set(0, method);
71+
}
6472

6573
/**
6674
* Get the "identity" method for this atomic test - the core {@link Test &#64;Test} method.

src/main/java/com/nordstrom/automation/junit/CreateTest.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,22 @@ public static Object intercept(@This final Object runner, @Argument(0) final Fra
6767

6868
if (0 == depthGauge.decreaseDepth()) {
6969
METHOD_DEPTH.get().remove(hashCode);
70+
createMappingsFor(runner, method, target);
71+
}
72+
73+
return target;
74+
}
75+
76+
/**
77+
* Create mappings for the specified test runner/test method/test class instance.
78+
*
79+
* @param runner underlying test runner
80+
* @param method target test method
81+
* @param target test class instance
82+
*/
83+
static void createMappingsFor(final Object runner, final FrameworkMethod method, final Object target) {
84+
// if mappings haven't been created
85+
if (getMethodFor(target) == null) {
7086
LOGGER.debug("testObjectCreated: {}", target);
7187
TARGET_TO_METHOD.put(toMapKey(target), method);
7288
TARGET_TO_RUNNER.put(toMapKey(target), runner);
@@ -77,15 +93,13 @@ public static Object intercept(@This final Object runner, @Argument(0) final Fra
7793
// if notifier hasn't been initialized yet
7894
if ( ! EachTestNotifierInit.setTestTarget(runner, method, target)) {
7995
// store target for subsequent retrieval
80-
HASHCODE_TO_TARGET.put(hashCode, target);
96+
HASHCODE_TO_TARGET.put(Objects.hash(runner, method.toString()), target);
8197
}
8298

8399
for (TestObjectWatcher watcher : LifecycleHooks.getObjectWatchers()) {
84100
watcher.testObjectCreated(runner, method, target);
85101
}
86102
}
87-
88-
return target;
89103
}
90104

91105
/**

src/main/java/com/nordstrom/automation/junit/EachTestNotifierInit.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,16 @@ public static void interceptor(@Argument(0) final RunNotifier notifier,
9898
* @param description description of the test that is about to be run
9999
* @return {@link AtomicTest} object
100100
*/
101-
private static AtomicTest newAtomicTestFor(Description description) {
102-
// create new atomic test object
103-
AtomicTest atomicTest = new AtomicTest(description);
104-
// create description => atomic test mapping
105-
DESCRIPTION_TO_ATOMICTEST.put(description.hashCode(), atomicTest);
101+
static AtomicTest newAtomicTestFor(Description description) {
102+
// get atomic test for this description
103+
AtomicTest atomicTest = getAtomicTestOf(description);
104+
// if none was found
105+
if (atomicTest == null) {
106+
// create new atomic test object
107+
atomicTest = new AtomicTest(description);
108+
// create description => atomic test mapping
109+
DESCRIPTION_TO_ATOMICTEST.put(description.hashCode(), atomicTest);
110+
}
106111

107112
return atomicTest;
108113
}

src/main/java/com/nordstrom/automation/junit/LifecycleHooks.java

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import java.util.concurrent.ConcurrentMap;
1313
import java.util.function.Function;
1414

15-
import org.apache.commons.lang3.reflect.MethodUtils;
15+
import org.apache.commons.lang3.ArrayUtils;
1616
import org.junit.internal.runners.model.ReflectiveCallable;
1717
import org.junit.runner.Description;
1818
import org.junit.runner.notification.RunListener;
@@ -337,12 +337,66 @@ static String getSubclassName(Object testObj) {
337337
@SuppressWarnings("unchecked")
338338
static <T> T invoke(Object target, String methodName, Object... parameters) {
339339
try {
340-
return (T) MethodUtils.invokeMethod(target, true, methodName, parameters);
340+
Method method = findMethod(target, methodName, parameters);
341+
return (T) method.invoke(target, parameters);
341342
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
342343
throw UncheckedThrow.throwUnchecked(e);
343344
}
344345
}
345346

347+
/**
348+
* Find the named method compatible with the supplied argument in the target object.
349+
*
350+
* @param target target object
351+
* @param methodName name of the desired method
352+
* @param parameters parameters for the method invocation
353+
* @return {@link Method} object for invocation
354+
* @throws NoSuchMethodException
355+
*/
356+
private static Method findMethod(Object target, String methodName, Object... parameters)
357+
throws NoSuchMethodException {
358+
Class<?> clazz = target.getClass();
359+
Class<?>[] paramTypes = new Class<?>[parameters.length];
360+
361+
for (int i = 0; i < parameters.length; i++) {
362+
paramTypes[i] = parameters[i].getClass();
363+
}
364+
365+
while (clazz != null) {
366+
for (Method method : clazz.getDeclaredMethods()) {
367+
if (method.getName().equals(methodName) &&
368+
isCompatible(method.getParameterTypes(), paramTypes)) {
369+
method.setAccessible(true);
370+
return method;
371+
}
372+
}
373+
clazz = clazz.getSuperclass(); // Move up the class hierarchy
374+
}
375+
376+
throw new NoSuchMethodException("Failed finding method " +
377+
methodName + " " + ArrayUtils.toString(paramTypes) +
378+
" in target " + target.getClass().getName());
379+
}
380+
381+
/**
382+
* Determine if declared method parameter types are compatible with provided parameters.
383+
*
384+
* @param declaredParams types of parameters declared by discovered method
385+
* @param providedParams types of provided parameters
386+
* @return {@code true} if declared parameters are compatible; otherwise {@code false}
387+
*/
388+
private static boolean isCompatible(Class<?>[] declaredParams, Class<?>[] providedParams) {
389+
if (declaredParams.length != providedParams.length) {
390+
return false;
391+
}
392+
for (int i = 0; i < declaredParams.length; i++) {
393+
if (!declaredParams[i].isAssignableFrom(providedParams[i])) {
394+
return false;
395+
}
396+
}
397+
return true;
398+
}
399+
346400
/**
347401
* Get the specified field of the supplied object.
348402
*

src/main/java/com/nordstrom/automation/junit/RetryHandler.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ public static Throwable runChildWithRetry(final Object runner, final FrameworkMe
6060
EachTestNotifier eachNotifier = new EachTestNotifier(notifier, description);
6161
AtomicTest atomicTest = EachTestNotifierInit.getAtomicTestOf(description);
6262

63+
// preserve original method
64+
atomicTest.setIdentity(method);
65+
6366
// if atomic test is theory
6467
if (atomicTest.isTheory()) {
6568
// retrieve cached method block statement
@@ -145,7 +148,8 @@ static int getMaxRetry(Object runner, final FrameworkMethod method) {
145148
NoRetry noRetryOnClass = method.getDeclaringClass().getAnnotation(NoRetry.class);
146149

147150
// if method isn't ignored or excluded from retry attempts
148-
if (Boolean.FALSE.equals(invoke(runner, "isIgnored", method)) && (noRetryOnMethod == null) && (noRetryOnClass == null)) {
151+
if (Boolean.FALSE.equals(invoke(runner, "isIgnored", method)) &&
152+
(noRetryOnMethod == null) && (noRetryOnClass == null)) {
149153
// get configured maximum retry count
150154
maxRetry = getConfig().getInteger(JUnitSettings.MAX_RETRY.key(), Integer.valueOf(0));
151155
}

src/main/java/com/nordstrom/automation/junit/RunReflectiveCall.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,16 +171,29 @@ private static boolean fireBeforeInvocation(Object runner, Object child, Reflect
171171
DepthGauge depthGauge = LifecycleHooks.computeIfAbsent(METHOD_DEPTH.get(), callable.hashCode(), NEW_INSTANCE);
172172
if (0 == depthGauge.increaseDepth()) {
173173
if (child instanceof FrameworkMethod) {
174-
Description description = LifecycleHooks.describeChild(runner, child);
174+
FrameworkMethod method = (FrameworkMethod) child;
175+
Description description = LifecycleHooks.describeChild(runner, method);
175176
if (LOGGER.isDebugEnabled()) {
176177
try {
177-
LOGGER.debug("beforeInvocation: {}", (description != null) ? description : child);
178+
LOGGER.debug("beforeInvocation: {}", (description != null) ? description : method);
178179
} catch (Throwable t) {
179180
// nothing to do here
180181
}
181182
}
182183
if ((description != null) && AtomicTest.isTest(description)) {
183184
DESCRIPTION_TO_CALLABLE.put(description.hashCode(), callable);
185+
186+
// get target for description
187+
Object target = getTargetFor(description);
188+
// if target acquired
189+
if (target != null) {
190+
// ensure that test object creation is tracked
191+
CreateTest.createMappingsFor(runner, method, target);
192+
// ensure that description has matching atomic test
193+
EachTestNotifierInit.newAtomicTestFor(description);
194+
// ensure that description <=> target mappings are set
195+
EachTestNotifierInit.setTestTarget(runner, method, target);
196+
}
184197
}
185198
}
186199
for (MethodWatcher watcher : LifecycleHooks.getMethodWatchers()) {
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.nordstrom.automation.junit;
2+
3+
import static org.junit.Assert.assertEquals;
4+
5+
import java.util.List;
6+
import java.util.Map;
7+
import java.util.Map.Entry;
8+
import java.util.Optional;
9+
10+
import org.junit.Test;
11+
import org.junit.runner.RunWith;
12+
import org.junit.runners.model.FrameworkMethod;
13+
14+
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
15+
import com.google.testing.junit.testparameterinjector.TestParameters;
16+
17+
@RunWith(TestParameterInjector.class)
18+
public class ArtifactCollectorParamInjector extends TestBase {
19+
20+
@Override
21+
public Optional<Map<String, Object>> getParameters() {
22+
AtomicTest atomicTest = LifecycleHooks.getAtomicTestOf(this);
23+
FrameworkMethod method = atomicTest.getIdentity();
24+
25+
// get test method parameters
26+
Class<?>[] paramTypes = method.getMethod().getParameterTypes();
27+
28+
try {
29+
Object testInfo = LifecycleHooks.getFieldValue(method, "testInfo");
30+
List<Object> params = LifecycleHooks.getFieldValue(testInfo, "parameters");
31+
32+
// allocate named parameters array
33+
Param[] namedParams = new Param[params.size()];
34+
// populate named parameters array
35+
for (int i = 0; i < params.size(); i++) {
36+
Object param = params.get(i);
37+
Map<String, Object> paramValue = LifecycleHooks.getFieldValue(param, "value");
38+
Entry<String, Object> paramEntry = paramValue.entrySet().iterator().next();
39+
namedParams[i] = Param.param(paramEntry.getKey(), paramTypes[i].cast(paramEntry.getValue()));
40+
}
41+
42+
// return params map as Optional
43+
return Param.mapOf(namedParams);
44+
} catch (IllegalAccessException | NoSuchFieldException | SecurityException e) {
45+
return Optional.empty();
46+
}
47+
}
48+
49+
@Test
50+
@TestParameters("{input: 'first test'}")
51+
@TestParameters("{input: 'second test'}")
52+
public void parameterized(String input) {
53+
System.out.println("parameterized: input = [" + input + "]");
54+
assertEquals("first test", input);
55+
}
56+
}

0 commit comments

Comments
 (0)