Skip to content

Commit b6dec6a

Browse files
authored
Add unit test and docs for JUnitParams support (#69)
1 parent 82bf113 commit b6dec6a

File tree

4 files changed

+182
-7
lines changed

4 files changed

+182
-7
lines changed

README.md

Lines changed: 91 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ The hooks that enable **JUnit Foundation** test lifecycle notifications are inst
200200
```
201201

202202
#### Gradle Configuration for JUnit Foundation
203-
```
203+
```groovy
204204
// build.gradle
205205
...
206206
apply plugin: 'maven'
@@ -325,7 +325,6 @@ public class LoggingWatcher implements MethodWatcher<FrameworkMethod> {
325325
}
326326
}
327327
}
328-
329328
```
330329

331330
Note that the implementation in this method watcher uses the annotations attached to the method objects to determine the type of method they're intercepting. Because each test method can have multiple configuration methods (both before and after), you may need to define additional conditions to control when your implementation runs. Examples of additional conditions include method name, method annotation, or an execution flag.
@@ -355,7 +354,11 @@ The ability to run **JUnit** tests in parallel is provided by the JUnit 4 test r
355354

356355
For lifecycle notification subscribers that need access to test invocation parameters, **JUnit Foundation** defines the [ArtifactParams](https://github.com/Nordstrom/JUnit-Foundation/blob/master/src/main/java/com/nordstrom/automation/junit/ArtifactParams.java) interface. This interface, along with the [AtomIdentity](https://github.com/Nordstrom/JUnit-Foundation/blob/master/src/main/java/com/nordstrom/automation/junit/AtomIdentity.java) test rule, enables test classes to publish their invocation parameters.
357356

358-
###### Publishing and Accessing Invocation Parameters
357+
The following example demonstrates how to publish invocation parameters with the [Parameterized](https://junit.org/junit4/javadoc/4.12/org/junit/runners/Parameterized.html) runner. The implementation is relatively simple, due to the strategy used by the runner to inject parameters into the test methods.
358+
359+
The `Parameterized` runner relies on instance fields to inject parameter values, and requires the test implementer to provide a constructor that initializes these fields. Alternatively, the implementer can mark the instance fields with the `@Parameter` annotation.
360+
361+
###### Publishing Invocation Parameters - `Parameterized` Runner
359362
```java
360363
package com.nordstrom.example;
361364

@@ -364,17 +367,15 @@ import static org.junit.Assert.assertTrue;
364367
import static com.nordstrom.automation.junit.ArtifactParams.param;
365368

366369
import java.util.Map;
367-
import java.util.Optional;
368-
369370
import org.junit.Rule;
370371
import org.junit.Test;
371372
import org.junit.runner.Description;
372373
import org.junit.runner.RunWith;
373374
import org.junit.runners.Parameterized;
374375
import org.junit.runners.Parameterized.Parameters;
375-
376376
import com.nordstrom.automation.junit.ArtifactParams;
377377
import com.nordstrom.automation.junit.AtomIdentity;
378+
import com.google.common.base.Optional;
378379

379380
@RunWith(Parameterized.class)
380381
public class ExampleTest implements ArtifactParams {
@@ -405,7 +406,7 @@ public class ExampleTest implements ArtifactParams {
405406

406407
@Override
407408
public Optional<Map<String, Object>> getParameters() {
408-
return ArtifactParams.mapOf(param("input", input));
409+
return Param.mapOf(Param.param("input", input));
409410
}
410411

411412
@Test
@@ -419,6 +420,89 @@ public class ExampleTest implements ArtifactParams {
419420
}
420421
```
421422

423+
The following example demonstrates how to publish invocation parameters with the [JUnitParams](https://github.com/Pragmatists/JUnitParams) runner. The implementation is more complex than the `Parameterized` example above, due to the strategy used by the runner to inject parameters into the test methods.
424+
425+
The `JUnitParams` runner injects parameters via test method arguments, with values provided by a `val$params` field in the "callable" closures associated with each test method invocation. Unlike the `Parameterized` runner, where all test methods in the class run with the same set of values, the `JUnitParams` runner allows each test method in the class to run with its own unique set of values. This variability must be accounted for in the implementation of the `getParameters()` method.
426+
427+
The example below queries the method for its parameter types, then uses these to cast each injected value to its corresponding type. This implementation assigns generic ordinal names to the parameters, because Java 7 reflection doesn't expose the names of method arguments. These could be acquired via the Java 8 API, or explicit mappings between test method signatures and argument names could be provided.
428+
429+
###### Publishing Invocation Parameters - `JUnitParams` Runner
430+
```java
431+
package com.nordstrom.example;
432+
433+
import static org.junit.Assert.assertEquals;
434+
435+
import java.util.Map;
436+
import org.junit.Test;
437+
import org.junit.runner.RunWith;
438+
import org.junit.internal.runners.model.ReflectiveCallable;
439+
import org.junit.runners.model.FrameworkMethod;
440+
import junitparams.JUnitParamsRunner;
441+
import junitparams.Parameters;
442+
import com.nordstrom.automation.junit.ArtifactParams;
443+
import com.nordstrom.automation.junit.AtomIdentity;
444+
import com.google.common.base.Optional;
445+
446+
@RunWith(JUnitParamsRunner.class)
447+
public class ArtifactCollectorJUnitParams implements ArtifactParams {
448+
449+
@Rule
450+
public final AtomIdentity identity = new AtomIdentity(this);
451+
452+
@Override
453+
public AtomIdentity getAtomIdentity() {
454+
return identity;
455+
}
456+
457+
@Override
458+
public Description getDescription() {
459+
return identity.getDescription();
460+
}
461+
462+
@Override
463+
public Optional<Map<String, Object>> getParameters() {
464+
// get runner for this target
465+
Object runner = LifecycleHooks.getRunnerForTarget(this);
466+
// get atomic test of target runner
467+
AtomicTest<FrameworkMethod> test = LifecycleHooks.getAtomicTestOf(runner);
468+
// get "callable" closure of test method
469+
ReflectiveCallable callable = LifecycleHooks.getCallableOf(runner, test.getIdentity());
470+
// get test method parameters
471+
Class<?>[] paramTypes = test.getIdentity().getMethod().getParameterTypes();
472+
473+
try {
474+
// extract execution parameters from "callable" closure
475+
Object[] params = LifecycleHooks.getFieldValue(callable, "val$params");
476+
477+
// NOTE: The "callable" closure also includes:
478+
// * this$0 - test case FrameworkMethod object
479+
// * val$target - test class instance ('this')
480+
// Only 'val$params' is unavailable elsewhere.
481+
482+
// allocate named parameters array
483+
Param[] namedParams = new Param[params.length];
484+
// populate named parameters array
485+
for (int i = 0; i < params.length; i++) {
486+
// create array item with generic name
487+
namedParams[i] = Param.param("param" + i, paramTypes[i].cast(params[i]));
488+
}
489+
490+
// return params map as Optional
491+
return Param.mapOf(namedParams);
492+
} catch (IllegalAccessException | NoSuchFieldException e) {
493+
return Optional.absent();
494+
}
495+
}
496+
497+
@Test
498+
@Parameters({ "first test", "second test" })
499+
public void parameterized(String input) {
500+
System.out.println("parameterized: input = [" + input + "]");
501+
assertEquals("first test", input);
502+
}
503+
}
504+
```
505+
422506
#### Artifact capture for parameterized tests
423507

424508
For scenarios that require artifact capture of parameterized tests, the [ArtifactCollector](https://github.com/Nordstrom/JUnit-Foundation/blob/master/src/main/java/com/nordstrom/automation/junit/ArtifactCollector.java) class extends the [AtomIdentity](https://github.com/Nordstrom/JUnit-Foundation/blob/master/src/main/java/com/nordstrom/automation/junit/AtomIdentity.java) test rule. This enables artifact type implementations to access invocation parameters and **Description** object for the current `atomic test`. For more details, see the [Artifact Capture](#artifact-capture) section below.

pom.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
<bytebuddy.version>1.10.3</bytebuddy.version>
3737
<logback.version>1.2.3</logback.version>
3838
<guava.version>28.1-android</guava.version>
39+
<junitparams.version>1.1.1</junitparams.version>
3940
<toolchains-plugin.version>1.1</toolchains-plugin.version>
4041
<surefire-plugin.version>2.22.0</surefire-plugin.version>
4142
<source-plugin.version>3.2.0</source-plugin.version>
@@ -107,6 +108,11 @@
107108
<artifactId>guava</artifactId>
108109
<version>${guava.version}</version>
109110
</dependency>
111+
<dependency>
112+
<groupId>pl.pragmatists</groupId>
113+
<artifactId>JUnitParams</artifactId>
114+
<version>${junitparams.version}</version>
115+
</dependency>
110116
</dependencies>
111117
</dependencyManagement>
112118

@@ -145,6 +151,11 @@
145151
<groupId>com.google.guava</groupId>
146152
<artifactId>guava</artifactId>
147153
</dependency>
154+
<dependency>
155+
<groupId>pl.pragmatists</groupId>
156+
<artifactId>JUnitParams</artifactId>
157+
<scope>test</scope>
158+
</dependency>
148159
</dependencies>
149160

150161
<build>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.nordstrom.automation.junit;
2+
3+
import static org.junit.Assert.assertEquals;
4+
5+
import java.util.Map;
6+
7+
import org.junit.Test;
8+
import org.junit.runner.RunWith;
9+
import org.junit.internal.runners.model.ReflectiveCallable;
10+
import org.junit.runners.model.FrameworkMethod;
11+
12+
import junitparams.JUnitParamsRunner;
13+
import junitparams.Parameters;
14+
15+
import com.google.common.base.Optional;
16+
17+
@RunWith(JUnitParamsRunner.class)
18+
public class ArtifactCollectorJUnitParams extends TestBase {
19+
20+
@Override
21+
public Optional<Map<String, Object>> getParameters() {
22+
// get runner for this target
23+
Object runner = LifecycleHooks.getRunnerForTarget(this);
24+
// get atomic test of target runner
25+
AtomicTest<FrameworkMethod> test = LifecycleHooks.getAtomicTestOf(runner);
26+
// get "callable" closure of test method
27+
ReflectiveCallable callable = LifecycleHooks.getCallableOf(runner, test.getIdentity());
28+
// get test method parameters
29+
Class<?>[] paramTypes = test.getIdentity().getMethod().getParameterTypes();
30+
31+
try {
32+
// extract execution parameters from "callable" closure
33+
Object[] params = LifecycleHooks.getFieldValue(callable, "val$params");
34+
35+
// NOTE: The "callable" closure also includes:
36+
// * this$0 - test case FrameworkMethod object
37+
// * val$target - test class instance ('this')
38+
// Only 'val$params' is unavailable elsewhere.
39+
40+
// allocate named parameters array
41+
Param[] namedParams = new Param[params.length];
42+
// populate named parameters array
43+
for (int i = 0; i < params.length; i++) {
44+
// create array item with generic name
45+
namedParams[i] = Param.param("param" + i, paramTypes[i].cast(params[i]));
46+
}
47+
48+
// return params map as Optional
49+
return Param.mapOf(namedParams);
50+
} catch (IllegalAccessException | NoSuchFieldException e) {
51+
return Optional.absent();
52+
}
53+
}
54+
55+
@Test
56+
@Parameters({ "first test", "second test" })
57+
public void parameterized(String input) {
58+
System.out.println("parameterized: input = [" + input + "]");
59+
assertEquals("first test", input);
60+
}
61+
}

src/test/java/com/nordstrom/automation/junit/ArtifactCollectorTest.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,23 @@ public void verifyParameterizedCapture() {
127127
assertNull(watcher.getArtifactProvider().getCaptureState(), "Artifact provider capture state should be 'null'");
128128
assertNull(watcher.getArtifactPath(), "Artifact capture should not have been requested");
129129
}
130+
131+
@Test
132+
public void verifyJUnitParamsCapture() {
133+
RunListenerAdapter rla = new RunListenerAdapter();
134+
135+
JUnitCore runner = new JUnitCore();
136+
runner.addListener(rla);
137+
Result result = runner.run(ArtifactCollectorJUnitParams.class);
138+
assertFalse(result.wasSuccessful());
139+
140+
assertEquals(rla.getPassedTests().size(), 1, "Incorrect passed test count");
141+
assertEquals(rla.getFailedTests().size(), 1, "Incorrect failed test count");
142+
assertEquals(rla.getIgnoredTests().size(), 0, "Incorrect ignored test count");
143+
144+
Description description = rla.getPassedTests().get(0);
145+
UnitTestCapture watcher = ArtifactCollector.getWatcher(description, UnitTestCapture.class).get();
146+
assertNull(watcher.getArtifactProvider().getCaptureState(), "Artifact provider capture state should be 'null'");
147+
assertNull(watcher.getArtifactPath(), "Artifact capture should not have been requested");
148+
}
130149
}

0 commit comments

Comments
 (0)