Unit testing a stdin / stdout based Java program

Oli Zimpasser
3 min readJul 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 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>.

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
}

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.

Anyhow, you can find the code and its usage in this github repository:

--

--