Home > Mobile >  Testing standard in and out in java without anticipating the user input that will be written
Testing standard in and out in java without anticipating the user input that will be written

Time:09-03

I have the typical java class that reads a user input from the console, processes it, and answers accordingly. For example:

  • If the user types "hello"
  • The program answers "Hello world!"

etc.

public class ConsoleTalker
{
    private Scanner scanner;
    
    public boolean continueExecution;
    
    public ConsoleTalker()
    {
        this.scanner = new Scanner(System.in);
    }
    
    public void Start()
    {
        this.continueExecution = true;
        
        while(continueExecution)
        {
            String userInput = this.ReadUserInput();
            
            this.ExecuteAction(userInput);
        }
    }
    
    private String ReadUserInput()
    {
        return scanner.nextLine();
    }
    
    private void println(String text)
    {
        System.out.println(text);
    }

    private void ExecuteAction(String userInput)
    {
        // ...
    }

Only the .Start() method is public. So you start it and the program will go on until:

  • The user types "exit"
  • The program answers "Goodbye!", sets continueExecuting = false and the loop ends

I want to test this class. I have followed the suggestions found in this website: https://www.danvega.dev/blog/2020/12/16/testing-standard-in-out-java/

However, this presents a problem, which is that I need to write all the userInput beforehand:

@Test
    public void StartListening_WriteHello_AnswersHelloWorld() {
        // Arrange
            //here you see I need to anticipate I will write a hello and an exit during the conversation
        String userInput = "hello"   System.lineSeparator()   "exit";
        String expectedAnswer1 = "Hello world!";
        String expectedAnswer2 = "Goodbye!";
        
        
            // Redirect standard in
        ByteArrayInputStream bais = new ByteArrayInputStream(userInput.getBytes());
        System.setIn(bais);
        
            // Redirect standard out
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        PrintStream printStream = new PrintStream(baos);
        System.setOut(printStream);
        
        // Act
        var consoleTalker = new ConsoleTalker();
        consoleTalker.Start();
        
        // Assert
        String[] lines = baos.toString().split(System.lineSeparator());
        String actualAnswer1 = lines[lines.length-2];
        String actualAnswer2 = lines[lines.length-1];
        
        Assertions.assertEquals(expectedAnswer1, actualAnswer1);
        Assertions.assertEquals(expectedAnswer2, actualAnswer2);
        Assertions.assertFalse(consoleTalker.continueExecution);
    }

How can I intercalate inputs and outputs during the test, back and forth? Where I read the output and I write the next simulated user input based on that output, or I compute the next expected output based on previous outputs.

Some of the outputs from ConsoleTalker are not going to be predictable (for example, if it contains some sort of random answer, or ids). If I need to write all the userInput I will be using during the test beforehand, some scenarios are not going to be testable.

I can modify ConsoleTalker in order to make it more testable but, for the sake of the answer, please try to avoid:

  • turning the private methods into public.
  • any workaround where I anticipate the program output and can write all the user input beforehand. I really need to go back and forth in the test with the inputs and outputs.

CodePudding user response:

There are several ways, depending on what is actually being tested and whether the class under test can be modified. From the question I'll assume that you don't want to or can't change ConsoleTalker. I also assume that running the class under test in a separate process is not an option, because it should be tested during the build process.

Leaves me with PipedInputStream/PipedOutputStream, that is create a pair for each direction (read / write), redirect System.out and System.in and run ConsoleTalker in a separate thread, e. g.

public class ConsoleTalkerTest {
    private static PrintStream sOut;

    public static void main(String[] args) throws Exception {
        sOut = System.out;
        InputStream sIn = System.in;

        try (PipedOutputStream srcOut = new PipedOutputStream();
             PipedInputStream srcIn = new PipedInputStream();
             PipedInputStream pis = new PipedInputStream(srcOut);
             PipedOutputStream pos = new PipedOutputStream(srcIn);
             BufferedReader reader = new BufferedReader(new InputStreamReader(pis));
             PrintStream writer = new PrintStream(pos, true)) {
        
            System.setOut(new PrintStream(srcOut, true));
            System.setIn(srcIn);

            Thread th = new Thread(() -> {
                ConsoleTalker talker = new ConsoleTalker();
                talker.Start();
            });
            th.start();

            writer.println("Test");

            String response = reader.readLine();
            showResults("Test", response);
        
            writer.println("exit");
            response = reader.readLine();
            showResults("ABCDEF", response);
        } finally {
            System.setIn(sIn);
            System.setOut(sOut);
        }
    }

    static void showResults(String expected, String actual) {
        if (Objects.equals(expected, actual)) {
            sOut.println("Received expected answer "   expected);
        } else {
            sOut.println("Expected "   expected   " but got "   actual);
        }
    }
}
  • Related