Back to Blog

Unit testing a stdin / stdout based Java program

Unit testing a stdin / stdout based Java program

Originally published on Medium, July 4, 2022

How to (unit) test a Java program which takes input from stdin and writes its results to stdout?

JUnit 5 is not offering any support for this scenario out of the box. Of course it would be easy to just redirect stdin / stdout like that:

System.setIn(new FileInputStream("test_input.txt"));
OutputStream baos = new ByteArrayOutputStream();
System.setOut(new PrintStream(baos));

UserManagerApp.main(null); // this is our stdin-stdout program

// test expected output against baos

The problem is, that you don't get precise feedback if anything fails and you also don't test if the output was generated in return to a specific input line.

What we want to achieve

What we want to do is more like:

@Test
public void testMethod() {

    try (TestCase testCase = TestCase.build()
            .input("add-user John Quil").expect("user added")
            .input("add-user Anita Bath").expect("user added")
            .input("list-users").expect(
                     "user:", "1,John,Quil", "2,Anita,Bath")
            .input("del-user 1").expect("user deleted")
            .input("list-users").expect("user:", "2,Anita,Bath")
            .input("quit")) {

        UserManagerApp.main(null);

    }
}

This should test each input line, terminated by <enter>, against each expected output line, terminated by <enter>.

Implementation

This is a class implementing such a logic:

public class TestCase {

    class TestStep {
        List<String> inputs;
        List<String> expectedOutputs;

        TestStep(List<String> inputs) {
            this.inputs = inputs;
        }
    }

    class TestInputStream extends InputStream {

        @Override
        public int read(byte b[], int off, int len) {
            if (expectedQueueType != QueueType.INPUT) {
                //FAIL
            }
            List<String> inputs = testSteps.get(mainCounter).inputs;
            String inputString = inputs.get(readSubCounter) + "\n";
            readSubCounter++;

            if (readSubCounter == inputs.size()) {
                expectedQueueType = QueueType.OUTPUT;
            }
            ByteArrayInputStream bais =
                  new ByteArrayInputStream(inputString.getBytes());
            return bais.read(b, off, len);
        }
    }

    class TestOutputStream extends OutputStream {

        private String buffer = "";

        @Override
        public void write(byte[] b, int off, int len) {
            if (expectedQueueType != QueueType.OUTPUT) {
                // FAIL
            }
            buffer += new String(b, 0, len);
            if (buffer.contains("\n")) {
                // remove string to test from buffer (0...\n)
                int posNewline = buffer.indexOf("\n");
                String stringToTest
                                  = buffer.substring(0, posNewline);
                if (posNewline < buffer.length() - 1) {
                    buffer = buffer.substring(posNewline + 1);
                } else {
                    buffer = "";
                }
                // check string against expected result
                String expectedOutput
                          = testSteps.get(mainCounter)
                             .expectedOutputs.get(writeSubCounter);
                if (!stringToTest.equals(expectedOutput)) {
                    // FAIL
                }
                writeSubCounter++;
                // when all expected blocks are found -> to input
                if (writeSubCounter ==
                                testSteps.get(mainCounter)
                                         .expectedOutputs.size()) {
                    expectedQueueType = QueueType.INPUT;
                    writeSubCounter = 0;
                    readSubCounter = 0;
                    mainCounter++;
                }
            }
        }
    }

    enum QueueType {
        INPUT, OUTPUT
    }

    private int mainCounter;
    private int writeSubCounter;
    private int readSubCounter;

    private QueueType expectedQueueType = QueueType.INPUT;

    private List<TestStep> testSteps = new ArrayList<>();

    private TestCase() {
        System.setIn(new TestInputStream());
        System.setOut(new PrintStream(new TestOutputStream()));
    }

    public static TestCase build() {
        return new TestCase();
    }

    public TestCase input(String... input) {
        testSteps.add(new TestStep(Arrays.asList(input)));
        return this;
    }

    public TestCase expect(String... expectedOutput) {
        testSteps.get(testSteps.size() - 1)
                   .expectedOutputs = Arrays.asList(expectedOutput);
        return this;
    }

    // Removed some code not necessary for the core logic
    // see the github repo for the complete code
}

Known issues

A known issue is the missing possibility to reset the program between tests, so you also need to instantiate a custom ClassLoader and load UserManagerApp into this ClassLoader, so you can throw it away between tests easily.

Complete code

You can find the code and its usage in this GitHub repository: https://github.com/oglimmer/junit-stdin-stdout