Skip to content

Commit b783246

Browse files
authored
Merge pull request #25 from Nordstrom/pr/add-param-example
Refactor parameterized test support; add "DisplayName" annotation
2 parents 4850bf8 + 6a8ff40 commit b783246

File tree

10 files changed

+352
-54
lines changed

10 files changed

+352
-54
lines changed

README.md

Lines changed: 166 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,67 @@ Note that the implementation in this method watcher uses the annotations attache
325325

326326
As indicated previously, **JUnit Foundation** will automatically attach standard JUnit **RunListener** providers that are declared in the associated **ServiceLoader** provider configuration file (i.e. - **_org.junit.runner.notification.RunListener_**). Declared run listeners are attached to the **RunNotifier** supplied to the `run()` method of JUnit runners. This feature eliminates behavioral differences between the various test execution environments like Maven, Gradle, and native IDE test runners.
327327

328+
### Support for Parameterized Tests
329+
330+
**JUnit** provides a custom [Parameterized](https://junit.org/junit4/javadoc/4.12/org/junit/runners/Parameterized.html) runner for executing parameterized tests. Third-party solutions are also available, such as [JUnitParams](https://github.com/Pragmatists/JUnitParams) and [ParameterizedSuite](https://github.com/PeterWippermann/parameterized-suite). Each of these solutions has its own implementation strategy, and no common interface is provided to access the parameters supplied to each invocation of the test methods in the parameterized class.
331+
332+
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.
333+
334+
###### Publishing and Accessing Invocation Parameters
335+
```java
336+
package com.nordstrom.example;
337+
338+
import static org.junit.Assert.assertArrayEquals;
339+
340+
import com.nordstrom.automation.junit.ArtifactParams;
341+
import com.nordstrom.automation.junit.AtomIdentity;
342+
343+
import org.junit.Rule;
344+
import org.junit.Test;
345+
import org.junit.runner.Description;
346+
import org.junit.runner.RunWith;
347+
import org.junit.runners.Parameterized;
348+
import org.junit.runners.Parameterized.Parameters;
349+
350+
@RunWith(Parameterized.class)
351+
public class ExampleTest implements ArtifactParams {
352+
353+
private String input;
354+
355+
public ExampleTest(String input) {
356+
this.input = input;
357+
}
358+
359+
@Rule
360+
public final AtomIdentity identity = new AtomIdentity(this);
361+
362+
@Parameters
363+
public static Object[] data() {
364+
return new Object[] { "first test", "second test" };
365+
}
366+
367+
@Override
368+
public Description getDescription() {
369+
return identity.getDescription();
370+
}
371+
372+
@Override
373+
public Object[] getParameters() {
374+
return new Object[] { input };
375+
}
376+
377+
@Test
378+
public void parameterized() {
379+
System.out.println("invoking: " + getDescription().getMethodName());
380+
assertArrayEquals(getParameters(), identity.getParameters());
381+
}
382+
}
383+
```
384+
385+
#### Artifact capture for parameterized tests
386+
387+
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.
388+
328389
## Test Method Timeout Management
329390

330391
**JUnit** provides test method timeout functionality via the `timeout` parameter of the **`@Test`** annotation. With this parameter, you can set an explicit timeout interval in milliseconds on an individual test method. If a test fails to complete within the specified interval, **JUnit** terminates the test and throws **TestTimedOutException**.
@@ -352,7 +413,6 @@ Failed attempts of tests that are selected for retry are tallied as ignored test
352413

353414
* [ArtifactCollector](https://github.com/Nordstrom/JUnit-Foundation/blob/master/src/main/java/com/nordstrom/automation/junit/ArtifactCollector.java):
354415
**ArtifactCollector** is a JUnit [test watcher](http://junit.org/junit4/javadoc/latest/org/junit/rules/TestWatcher.html) that serves as the foundation for artifact-capturing test watchers. This is a generic class, with the artifact-specific implementation provided by instances of the **ArtifactType** interface. For artifact capture scenarios where you need access to the current method description or the values provided to parameterized tests, the test class can implement the **ArtifactParams** interface.
355-
356416
* [ArtifactParams](https://github.com/Nordstrom/JUnit-Foundation/blob/master/src/main/java/com/nordstrom/automation/junit/ArtifactParams.java):
357417
By implementing the **ArtifactParams** interface in your test classes, you enable the artifact capture framework to access test method description objects and parameterized test values. These can be used for composing, naming, and storing artifacts.
358418
* [ArtifactType](https://github.com/Nordstrom/JUnit-Foundation/blob/master/src/main/java/com/nordstrom/automation/junit/ArtifactType.java):
@@ -385,8 +445,8 @@ public class MyArtifactType implements ArtifactType {
385445
@Override
386446
public byte[] getArtifact(Object instance, Throwable reason) {
387447
if (instance instanceof ArtifactParams) {
388-
ArtifactParams params = (ArtifactParams) instance;
389-
return String.format(ARTIFACT, params.getDescription().getMethodName()).getBytes().clone();
448+
ArtifactParams publisher = (ArtifactParams) instance;
449+
return String.format(ARTIFACT, publisher.getDescription().getMethodName()).getBytes().clone();
390450
} else {
391451
return new byte[0];
392452
}
@@ -422,17 +482,18 @@ public class MyArtifactCapture extends ArtifactCollector<MyArtifactType> {
422482
public MyArtifactCapture(Object instance) {
423483
super(instance, new MyArtifactType());
424484
}
425-
426485
}
427486
```
428487

429488
The preceding code is an example of how the artifact type definition can be assigned as the type parameter in a subclass of **ArtifactCollector**. This isn't strictly necessary, but will make your code more concise, as the next example demonstrates. This technique also provides the opportunity to extend or alter the basic artifact capture behavior.
430489

431490
###### Attaching artifact collectors to test classes
432-
433491
```java
434492
package com.nordstrom.example;
435493

494+
import com.nordstrom.automation.junit.ArtifactCollector;
495+
import com.nordstrom.automation.junit.ArtifactParams;
496+
436497
import org.junit.Rule;
437498
import org.junit.runner.Description;
438499

@@ -448,9 +509,107 @@ public class ExampleTest implements ArtifactParams {
448509

449510
@Override
450511
public Description getDescription() {
451-
return watcher.getDescription();
512+
return watcher1.getDescription();
452513
}
453514
}
454515
```
455516

456-
This example demonstrates two techniques for attaching artifact collectors to test classes. Either technique will activate basic artifact capture functionality. Of course, the first option is required to activate extended behavior implemented in a type-specific subclass of **ArtifactCapture**.
517+
This example demonstrates two techniques for attaching artifact collectors to test classes. Either technique will activate basic artifact capture functionality. Of course, the first option is required to activate extended behavior implemented in a type-specific subclass of **ArtifactCollector**.
518+
519+
### Parameter-Aware Artifact Capture
520+
521+
Although the **ExampleTest** class in the previous section implements the [ArtifactParams](https://github.com/Nordstrom/JUnit-Foundation/blob/master/src/main/java/com/nordstrom/automation/junit/ArtifactParams.java) interface, this non-parameterized example only shows half of the story. In addition to the **Description** objects of `atomic tests`, classes that implement parameterized tests are able to publish their invocation parameters, and the artifact capture feature is able to access them.
522+
523+
The following example extends the previous artifact type to add parameter-awareness:
524+
525+
###### Parameter-aware artifact type
526+
```java
527+
class MyParameterizedType extends MyArtifactType {
528+
529+
@Override
530+
public byte[] getArtifact(Object instance, Throwable reason) {
531+
if (instance instanceof ArtifactParams) {
532+
ArtifactParams publisher = (ArtifactParams) instance;
533+
StringBuilder artifact = new StringBuilder("method: ")
534+
.append(publisher.getDescription().getMethodName()).append("\n");
535+
int i = 0;
536+
for (Object param : publisher.getParameters()) {
537+
"param" + i++ + ": [" + param + "]\n";
538+
}
539+
return artifact.toString().getBytes().clone();
540+
} else {
541+
return new byte[0];
542+
}
543+
}
544+
}
545+
```
546+
547+
Notice the call to `getParameters()`, which retrieves the invocation parameters published by the test class instance. There's also a call to `getDescription()`, which returns the **Description** object for the current `atomic test`.
548+
549+
The implementation below composes a type-specific subclass of **ArtifactCollector** to produce a parameter-aware artifact collector:
550+
551+
###### Parameter-aware artifact collector
552+
```java
553+
package com.nordstrom.example;
554+
555+
import com.nordstrom.automation.junit.ArtifactCollector;
556+
557+
public class MyParameterizedCapture extends ArtifactCollector<MyParameterizedType> {
558+
559+
public MyParameterizedCapture(Object instance) {
560+
super(instance, new MyParameterizedType());
561+
}
562+
}
563+
```
564+
565+
The following example implements a parameterized test class that publishes its invocation parameters through the **ArtifactParams** interface. It uses the custom **Parameterized** runner to invoke the `parameterized()` test method twice - once with input "first test", and once with input "second test". The test class constructor accepts the invocation parameter as its argument and stores it in an instance field for use by the test.
566+
567+
* The `getDescription()` method acquires the **Description** object for the current `atomic test` from the `watcher` test rule.
568+
* The `getParameters()` method assembles the array of invocation parameters from the `input` instance field populated by the constructor.
569+
570+
###### Parameterized test class
571+
```java
572+
package com.nordstrom.example;
573+
574+
import com.nordstrom.automation.junit.ArtifactParams;
575+
576+
import org.junit.Rule;
577+
import org.junit.Test;
578+
import org.junit.runner.Description;
579+
import org.junit.runner.RunWith;
580+
import org.junit.runners.Parameterized;
581+
import org.junit.runners.Parameterized.Parameters;
582+
583+
@RunWith(Parameterized.class)
584+
public class ParameterizedTest implements ArtifactParams {
585+
586+
@Rule
587+
public final MyParameterizedCapture watcher = new MyParameterizedCapture(this);
588+
589+
private String input;
590+
591+
public ParameterizedTest(String input) {
592+
this.input = input;
593+
}
594+
595+
@Parameters
596+
public static Object[] data() {
597+
return new Object[] { "first test", "second test" };
598+
}
599+
600+
@Override
601+
public Description getDescription() {
602+
return watcher.getDescription();
603+
}
604+
605+
@Override
606+
public Object[] getParameters() {
607+
return new Object[] { input };
608+
}
609+
610+
@Test
611+
public void parameterized() {
612+
assertArrayEquals(getParameters(), watcher.getParameters());
613+
}
614+
}
615+
```

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
3030
<maven.compiler.source>1.8</maven.compiler.source>
3131
<maven.compiler.target>1.8</maven.compiler.target>
32-
<java-utils.version>1.7.2</java-utils.version>
32+
<java-utils.version>1.7.3</java-utils.version>
3333
<surefire-plugin.version>2.22.0</surefire-plugin.version>
3434
<source-plugin.version>3.0.1</source-plugin.version>
3535
<javadoc-plugin.version>2.10.4</javadoc-plugin.version>

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

Lines changed: 20 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,24 @@
1010
import java.util.Optional;
1111
import java.util.concurrent.ConcurrentHashMap;
1212

13-
import org.junit.rules.TestWatcher;
1413
import org.junit.runner.Description;
15-
1614
import com.nordstrom.common.file.PathUtils;
1715

1816
/**
1917
* This is the base class for implementations of scenario-specific artifact collectors.
2018
*
2119
* @param <T> scenario-specific artifact type
2220
*/
23-
public class ArtifactCollector<T extends ArtifactType> extends TestWatcher {
21+
public class ArtifactCollector<T extends ArtifactType> extends AtomIdentity {
2422

2523
private static final Map<Description, List<ArtifactCollector<? extends ArtifactType>>> watcherMap =
2624
new ConcurrentHashMap<>();
2725

2826
private final T provider;
29-
private final Object instance;
30-
private Description description;
3127
private final List<Path> artifactPaths = new ArrayList<>();
3228

3329
public ArtifactCollector(Object instance, T provider) {
34-
this.instance = instance;
30+
super(instance);
3531
this.provider = provider;
3632
}
3733

@@ -40,12 +36,9 @@ public ArtifactCollector(Object instance, T provider) {
4036
*/
4137
@Override
4238
public void starting(Description description) {
43-
this.description = description;
44-
List<ArtifactCollector<? extends ArtifactType>> watcherList = watcherMap.get(description);
45-
if (watcherList == null) {
46-
watcherList = new ArrayList<>();
47-
watcherMap.put(description, watcherList);
48-
}
39+
super.starting(description);
40+
List<ArtifactCollector<? extends ArtifactType>> watcherList =
41+
watcherMap.computeIfAbsent(description, k -> new ArrayList<>());
4942
watcherList.add(this);
5043
}
5144

@@ -64,11 +57,11 @@ public void failed(Throwable e, Description description) {
6457
* @return (optional) path at which the captured artifact was stored
6558
*/
6659
public Optional<Path> captureArtifact(Throwable reason) {
67-
if (! provider.canGetArtifact(instance)) {
60+
if (! provider.canGetArtifact(getInstance())) {
6861
return Optional.empty();
6962
}
7063

71-
byte[] artifact = provider.getArtifact(instance, reason);
64+
byte[] artifact = provider.getArtifact(getInstance(), reason);
7265
if ((artifact == null) || (artifact.length == 0)) {
7366
return Optional.empty();
7467
}
@@ -79,7 +72,7 @@ public Optional<Path> captureArtifact(Throwable reason) {
7972
Files.createDirectories(collectionPath);
8073
} catch (IOException e) {
8174
String messageTemplate = "Unable to create collection directory ({}); no artifact was captured";
82-
provider.getLogger().warn(messageTemplate, collectionPath, e);
75+
Optional.ofNullable(provider.getLogger()).ifPresent(l -> l.warn(messageTemplate, collectionPath, e));
8376
return Optional.empty();
8477
}
8578
}
@@ -91,15 +84,18 @@ public Optional<Path> captureArtifact(Throwable reason) {
9184
getArtifactBaseName(),
9285
provider.getArtifactExtension());
9386
} catch (IOException e) {
94-
provider.getLogger().warn("Unable to get output path; no artifact was captured", e);
87+
Optional.ofNullable(provider.getLogger()).ifPresent(
88+
l -> l.warn("Unable to get output path; no artifact was captured", e));
9589
return Optional.empty();
9690
}
9791

9892
try {
99-
provider.getLogger().info("Saving captured artifact to ({}).", artifactPath);
93+
Optional.ofNullable(provider.getLogger()).ifPresent(
94+
l -> l.info("Saving captured artifact to ({}).", artifactPath));
10095
Files.write(artifactPath, artifact);
10196
} catch (IOException e) {
102-
provider.getLogger().warn("I/O error saving to ({}); no artifact was captured", artifactPath, e);
97+
Optional.ofNullable(provider.getLogger()).ifPresent(
98+
l -> l.warn("I/O error saving to ({}); no artifact was captured", artifactPath, e));
10399
return Optional.empty();
104100
}
105101

@@ -113,8 +109,8 @@ public Optional<Path> captureArtifact(Throwable reason) {
113109
* @return path of artifact storage directory
114110
*/
115111
private Path getCollectionPath() {
116-
Path collectionPath = PathUtils.ReportsDirectory.getPathForObject(instance);
117-
return collectionPath.resolve(provider.getArtifactPath(instance));
112+
Path collectionPath = PathUtils.ReportsDirectory.getPathForObject(getInstance());
113+
return collectionPath.resolve(provider.getArtifactPath(getInstance()));
118114
}
119115

120116
/**
@@ -127,16 +123,12 @@ private Path getCollectionPath() {
127123
* @return artifact file base name
128124
*/
129125
private String getArtifactBaseName() {
130-
Object[] parameters = new Object[0];
131-
if (instance instanceof ArtifactParams) {
132-
parameters = ((ArtifactParams) instance).getParameters();
133-
}
134-
if (parameters.length == 0) {
135-
return description.getMethodName();
126+
if (getParameters().length == 0) {
127+
return getDescription().getMethodName();
136128
} else {
137-
int hashcode = Arrays.deepHashCode(parameters);
129+
int hashcode = Arrays.deepHashCode(getParameters());
138130
String hashStr = String.format("%08X", hashcode);
139-
return description.getMethodName() + "-" + hashStr;
131+
return getDescription().getMethodName() + "-" + hashStr;
140132
}
141133
}
142134

@@ -171,15 +163,6 @@ public T getArtifactProvider() {
171163
return provider;
172164
}
173165

174-
/**
175-
* Get the JUnit {@link Description} object associated with this artifact collector.
176-
*
177-
* @return JUnit method description object
178-
*/
179-
public Description getDescription() {
180-
return description;
181-
}
182-
183166
/**
184167
* Get reference to an instance of the specified watcher type associated with the described method.
185168
*

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
public interface ArtifactParams {
99

1010
/**
11-
* Get get JUnit method description object for the current test class instance.
11+
* Get the JUnit method description object for the current test class instance.
1212
*
1313
* @return JUnit method description object
1414
*/

0 commit comments

Comments
 (0)