The standard IO extension adds a simple way to test classes that read from the standard input (System.in) or write to the standard output (System.out).

Warning
Depending on the configuration, the extension redirects the standard input and/or output, in which case nothing gets forwarded to the original System.in and/or System.out. This becomes particularly important when running tests in parallel, where other tests may interfere with tests annotated with @StdIo.

Basic use

The extension consists of two parts:

  • The annotation @StdIo. It allows defining input that is read from System.in without having to wait for user input.

  • Parameters StdIn and StdOut, which you can have injected into your test. Their capturedLines() methods allow you to access lines read from System.in or written to System.out, so you can verify them with common assertions.

For example, after calling System.out.println("Hello") and System.out.println("World"), the StdOut::capturedLines method would return an array ["Hello", "World"]. With System.out.print("Hello") and System.out.println("World") (note that the first method does not print a line break), it would return ["HelloWorld"].

Here’s which combinations of this annotation (with or without values for the read lines) and parameters are valid:

@StdIo("…​")

In this case System.in gets replaced and the code under test will read the specified lines (in the snippet, that’s just the line "…​").

public class StandardInputOutputTests {

    @Test
    @StdIo({"Hello", "World"})
    void test() {
        // `System.in` is replaced and the code under
        // test reads lines "Hello" and "World"
    }

}
@StdIo("…​") and StdIn

Like before, but the lines read by the code under test can be verified with the StdIn parameter.

public class StandardInputOutputTests {

    @Test
    @StdIo({"Hello", "World"})
    void test(StdIn in) {
        // `System.in` is replaced, the code under
        // test reads lines "Hello" and "World",
        // and `StdIn` can be used to verify that
    }

}
@StdIo and StdOut

System.in is not replaced (because no input lines are defined), but System.out is, so the written lines can be verified with the StdOut parameter.

public class StandardInputOutputTests {

    @Test
    @StdIo
    void test(StdOut out) {
        // `System.out` is replaced, so the written lines
        // are captured and can be verified with `StdOut`
    }
@StdIo("…​") and StdOut

System.in is replaced, so the code can read the input lines and System.out is replaced, so the written lines can be verified with the StdOut parameter.

public class StandardInputOutputTests {

    @Test
    @StdIo()
    void test(StdOut out) {
        // `System.in` and `System.out` are replaced
        // and written lines can be verified with `StdOut`
    }

}
@StdIo("…​"), StdIn, and StdOut

A combination of the two previous cases - System.in and System.out get replaced.

public class StandardInputOutputTests {

    @Test
    @StdIo({"Hello", "World"})
    void test(StdIn in, StdOut out) {
        // `System.in` is replaced, the code under
        // test reads lines "Hello" and "World",
        // and `StdIn` can be used to verify that;
        // `System.out` is also replaced, so the
        // written lines can be verified with `StdOut`
    }

}

The remaining combinations of the annotation, its values, and StdIn/StdOut are considered misconfigurations and lead to exceptions.

Thread-Safety

Since System.in and System.out are global state, reading and writing them during parallel test execution can lead to unpredictable results and flaky tests. The @StdIo extension is prepared for that and tests annotated with it will never execute in parallel (thanks to resource locks) to guarantee correct test results.

However, this does not cover all possible cases. Tested code that reads System.in or System.out or calls System.setIn() or System.setOut() independently of the extensions can still run in parallel to them and may thus behave erratically when, for example, it unexpectedly uses System.out that was set by the extension in another thread. Tests that cover code that reads or writes System.in or System.out need to be annotated with the respective annotation:

  • @ReadsStdIo

  • @WritesStdIo

Tests annotated in this way will never execute in parallel with tests annotated with @StdIo.

Edge cases and unexpected behavior

Empty input (with or without StdIn)

Using just @StdIo does not redirect System.in, which means code reading from it will still block until input is provided and a StdIn parameter can’t be resolved. To have the code under test read empty input and/or use StdIn to verify that, use @StdIo("").

Unexpected behavior with eager/buffering readers

Some readers read all lines from StdIo eagerly (e.g.: BufferedReader) which can lead to unexpected behavior. Take the following example:

class ExampleConsoleReader {

    private List<String> lines = new ArrayList<>();

    public void readLines() {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        for (int i = 0; i < 2; i++) {
            String line = reader.readLine();
            lines.add(line);
        }
    }

}

This is a straightforward example class. It reads two lines from System.in, using a BufferedReader. This is the unit test for this class, using StdIoExtension:

class ExampleConsoleReaderTest {

    @Test
    @StdIo({ "line1", "line2", "line3" })
    void testReadLines(StdIn in) {
        ExampleConsoleReader consoleReader = new ExampleConsoleReader();

        consoleReader.readLines();

        // assertEquals(in.capturedLines(), "line1", "line2"); // This is failing
        // assertEquals(in.capturedLines(), "line1", "line2", "line3"); // This is passing
    }

}

The underlying BufferedReader eagerly reads all three supplied lines during the first readLine call in the loop (that’s why it’s called buffered reader). This means that the assertion fails, because in.capturedLines() contains three lines - even though consoleReader.lines only contains two.