Making test code readable with JUnit Pioneer
Browsing a project's test cases is a common approach to better understanding the codebase. Making our tests readable has a tremendous impact on this approach's feasibility. For that, JUnit Pioneer is a mandatory tool in your toolset!
This article won't cover everything included with JUnit Pioneer. It's all pretty well documented.
Let's take the following example:
class App {
private static final Set<String> acceptableAnswers = Set.of("yes", "sure");
public static void main(final String... args) {
System.out.println("Hello, do you love Java?");
verifyResponse(new Scanner(System.in));
System.out.println("Me too!");
}
private static void verifyResponse(final Scanner scanner) {
if (!acceptableAnswers.contains(scanner.next().toLowerCase())) {
System.out.println("""
Hmmm... I'm not sure I understand.
Please reply with 'yes' or 'sure'.
Do you love Java?""");
verifyResponse(scanner);
}
}
}
This is pretty straightforward, executing the main method will ask: "Hello, do you love Java?" and will not accept any answers other than "yes" or "sure".
The common approach for testing code that takes data from the System's in
stream and prints it to its out
stream is to hijack the streams:
class Manual_Hijack_System_Streams_Test {
private InputStream origIn;
private PrintStream origOut;
private OutputStream output;
@BeforeEach
void initialize() {
origIn = System.in;
origOut = System.out;
output = new ByteArrayOutputStream();
System.setOut(new PrintStream(output));
}
@AfterEach
void cleanup() {
System.setIn(origIn);
System.setOut(origOut);
}
@ParameterizedTest
@ValueSource(strings = {"yes", "sure"})
void verify_replying_with_an_acceptable_answer(final String answer) {
System.setIn(inputStreamOf(answer));
App.main(new String[0]);
assertArrayEquals(
new String[] {"Hello, do you love Java?", "Me too!"},
output.toString().split(System.lineSeparator()));
}
@Test
void verify_replying_with_a_wrong_and_an_acceptable_answer() {
System.setIn(inputStreamOf(collectAnswers("no", "yes")));
App.main(new String[0]);
assertArrayEquals(
new String[] {
"Hello, do you love Java?",
"""
Hmmm... I'm not sure I understand.
Please reply with 'yes' or 'sure'.
Do you love Java?""",
"Me too!"},
output.toString().split(System.lineSeparator()));
}
private InputStream inputStreamOf(final String input) {
return new ByteArrayInputStream(input.getBytes(UTF_8));
}
private String collectAnswers(final String... answers) {
return stream(answers).collect(joining(System.lineSeparator()));
}
}
With JUnit Pioneer, we don't need to worry about hijacking the streams:
class Let_Junit_Pioneer_Do_Its_Thing_Test {
@ParameterizedTest
@ValueSource(strings = {"yes", "sure"})
@StdIo
void verify_replying_with_an_acceptable_answer(final String answer, final StdOut output) {
var origIn = System.in;
System.setIn(inputStreamOf(answer));
App.main(new String[0]);
assertArrayEquals(
new String[] {"Hello, do you love Java?", "Me too!"},
output.capturedLines());
System.setIn(origIn);
}
@Test
@StdIo({"no", "yes"})
void verify_replying_with_a_wrong_and_an_acceptable_answer(final StdOut output) {
App.main(new String[0]);
assertArrayEquals(
new String[] {
"Hello, do you love Java?",
"""
Hmmm... I'm not sure I understand.
Please reply with 'yes' or 'sure'.
Do you love Java?""",
"Me too!"},
output.capturedLines());
}
private InputStream inputStreamOf(final String input) {
return new ByteArrayInputStream(input.getBytes(UTF_8));
}
}
For the second test method, verify_replying_with_a_wrong_and_an_acceptable_answer
, we're using the StdIo
annotation to hijack the in
stream and specify the value to be captured by the stream to two lines, "no" and then "yes."
We also use the arguments resolver to inject our method with a hijacked out
stream, which we can use to validate the result.
For the first test method, verify_replying_with_an_acceptable_answer
, we're using a propertyless StdIo
annotation. This annotation marks our test case for the arguments resolver, but we don't need it to hijack the in
stream.
However, we make it hijack the out
stream by instructing the arguments resolver to inject it so we can validate the results, as we did in the second test method.
The reason is that the test container produces multiple test cases. One with "yes" as the answer and one with "sure."
This means that the value specified for the in
stream differs for each case, meaning we had to hijack the in
stream ourselves for this specific test.
Executing any of the above test classes will output the following:
[INFO] '-- JUnit Jupiter [OK]
[INFO] +-- Manual Hijack System Streams Test [OK]
[INFO] | +-- verify replying with a wrong and an acceptable answer [OK]
[INFO] | '-- verify replying with an acceptable answer (String) [OK]
[INFO] | +-- [1] yes [OK]
[INFO] | '-- [2] sure [OK]
[INFO] '-- Let Junit Pioneer Do Its Thing Test [OK]
[INFO] +-- verify replying with a wrong and an acceptable answer (StdOut) [OK]
[INFO] '-- verify replying with an acceptable answer (String, StdOut) [OK]
[INFO] +-- [1] yes [OK]
[INFO] '-- [2] sure [OK]
Checkout a previous article of mine, Property-Based Matrix Testing in Java and see how we can turn this:
@ParameterizedTest
@MethodSource("getArguments")
void using_junit_parameterized_test_with_method_source(
final Direction direction, final Status status) {
assertTrue(true);
}
static Stream<Arguments> getArguments() {
return Stream.of(Direction.values())
.flatMap(d -> Stream.of(Status.values()).map(s -> arguments(d, s)));
}
Into this:
@CartesianProductTest
@CartesianEnumSource(Direction.class)
@CartesianEnumSource(Status.class)
void using_junit_pioneer_cartesian_product_test_with_enum_source(
final Direction direction, final Status status) {
assertTrue(true);
}
Check out the code for this tutorial on Github.
Top comments (0)