Search code examples
javaunit-testingstdin

How might I test that a CLI app waits for input


This is not the same as this question: JUnit: How to simulate System.in testing?, which is about mocking stdin.

What I want to know is how to test (as in TDD) that a simple Java class with a main method waits for input.

My test:

@Test 
public void appRunShouldWaitForInput(){
    long startMillis = System.currentTimeMillis();
    // NB obviously you'd want to run this next line in a separate thread with some sort of timeout mechanism... 
    // that's an implementation detail I've omitted for the sake of avoiding clutter!
    App.main( null );
    long endMillis = System.currentTimeMillis();
    assertThat( endMillis - startMillis ).isGreaterThan( 1000L );
}

My SUT main:

public static void main(String args[]) {
    BufferedReader br = null;
    try {
        br = new BufferedReader(new InputStreamReader(System.in));
        System.out.print("Enter something : ");
        String input = br.readLine();
    } catch (IOException e) {
        e.printStackTrace();
    } 

... test fails. The code does not wait. But when you run the app at the command prompt it does indeed wait.

NB by the way I did also try with setting stdin to sthg else:

System.setIn(new ByteArrayInputStream( dummy.getBytes()));
scanner = new Scanner(System.in);

... this did not hold up the test.


Solution

  • As a much more general rule, static methods (such as main methods) are difficult to test. For this reason, you almost never call the main method (or any other static method) from your test code. A common pattern to work around this is to convert this:

    public class App {
        public static void main(String args[]) {
            BufferedReader br = null;
            try {
                br = new BufferedReader(new InputStreamReader(System.in));
                System.out.print("Enter something : ");
                String input = br.readLine();
            } catch (IOException e) {
                e.printStackTrace();
            } 
        }
    }
    

    to this:

    public class App {
        private final InputStream input;
        private final OutputStream output;
    
        public App(InputStream input, OutputStream output) {
            this.input = input;
            this.output = output;
        }
    
        public static void main(String[] args) {
            new App(System.in, System.out).start();
        }
    
        public void start() {
            BufferedReader br = null;
            try {
                br = new BufferedReader(new InputStreamReader(input));
                output.print("Enter something : ");
                String nextInput = br.readLine();
            } catch (IOException e) {
                e.printStackTrace();
            } 
        }
    }
    

    Now your test becomes this:

    @Test 
    public void appRunShouldWaitForInput(){
        ByteArrayOutputStream output = new ByteArrayOutputStream();
    
        // As you have already noted, you would need to kick this off on another thread and use a blocking implementation of InputStream to test what you want to test.
        new App(new ByteArrayInputStream(), output).start();
    
        assertThat(output.toByteArray().length, is(0));
    }
    

    The key idea is that, when you run the app "for real" i.e. via the main method, it will use the Standard input and output streams. However, when you run it from your tests, it uses a purely in memory input/output stream which you have full control over in your test. ByteArrayOutputStream is just one example, but you can see in my example that the test is able to inspect the actual bytes that have been written to the output stream.