DEV Community

Cover image for Testing Java with JUnit Pioneer
Tomer Figenblat
Tomer Figenblat

Posted on • Edited on

Testing Java with JUnit Pioneer

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);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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()));
  }
}

Enter fullscreen mode Exit fullscreen mode

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));
  }
}
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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)));
  }
Enter fullscreen mode Exit fullscreen mode

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);
  }
Enter fullscreen mode Exit fullscreen mode

Check out the code for this tutorial on Github.

Top comments (0)