Testing console based applications/programs - Java

2019-01-11 11:24发布

All,

I have written a PhoneBook application in Java that is command line based. The application basically asks for some details of user like Name, Age, Address and phone numbers and stores them in a file. Other operations involve looking up PhoneBook by name, phone number etc. All the details are entered through console.

I am trying to write JUnit test cases for each of the functionalities that I have implemented but not able to figure out how to redirect System.in in the implementation code to something in my JUnit test methods that would supply those values when my actual code stops for user input?

Example:

My implementation code has:

BufferedReader is = new BufferedReader (new InputStreamReader(System.in));
System.out.println("Please enter your name:");
String name = is.readLine();             // My test cases stop at this line. How can I pass command line values i.e. redirect System.in to my test based values?

Hope it makes sense

5条回答
太酷不给撩
2楼-- · 2019-01-11 12:00

This takes a basic looping console application and makes it testable, using the ideas from oxbow_lakes' answer.

The class-proper:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;

public class TestableLoopingConsoleExample {

   public static final String INPUT_LINE_PREFIX = "> ";
   public static final String EXIT_COMMAND = "exit";
   public static final String RESPONSE_PLACEHOLDER = "...response goes here...";
   public static final String EXIT_RESPONSE = "Exiting.";

   public static void main(String[] cmdLineParams_ignored) throws IOException {
      BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
      PrintStream out = new PrintStream(System.out);
      PrintStream err = new PrintStream(System.err);

      try {
         new TestableLoopingConsoleExample().main(cmdLineParams_ignored, in, out);
      } catch (Exception e) {  //For real use, catch only the exactly expected types
         err.println(e.toString());
      }
   }

...continued...

   public void main(String[] cmdLineParams_ignored, BufferedReader in, PrintStream out)
         throws IOException {

      System.out.println("Enter some text, or '" + EXIT_COMMAND + "' to quit");

      while (true) {

         out.print(INPUT_LINE_PREFIX);
         String input = in.readLine();
         out.println(input);

         if (input.length() == EXIT_COMMAND.length() &&
            input.toLowerCase().equals(EXIT_COMMAND)) {

            out.println(EXIT_RESPONSE);
            return;
         }

         out.println(RESPONSE_PLACEHOLDER);
      }
   }
}

The test (JUnit4):

import static org.junit.Assert.assertEquals;
import static testableloopingconsoleapp.TestableLoopingConsoleExample.EXIT_COMMAND;
import static testableloopingconsoleapp.TestableLoopingConsoleExample.EXIT_RESPONSE;
import static testableloopingconsoleapp.TestableLoopingConsoleExample.INPUT_LINE_PREFIX;
import static testableloopingconsoleapp.TestableLoopingConsoleExample.RESPONSE_PLACEHOLDER; 

import org.junit.Before;
import org.junit.Test; 

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.io.StringReader; 

public class TestableLoopingConsoleExampleTest { 

  private final ByteArrayOutputStream out = new ByteArrayOutputStream();
  private final ByteArrayOutputStream err = new ByteArrayOutputStream(); 

  @Before
  public final void resetOutputStreams() {
     out.reset();
     err.reset();
  } 

...continued...

  @Test
  public void testableMain_validInputFromString_outputAsExpected() throws Exception {
     String line1 = "input line 1\n";
     String line2 = "input line 2\n";
     String line3 = "input line 3\n";
     String exitLine = EXIT_COMMAND + "\n"; 

     BufferedReader in = new BufferedReader(new StringReader(
         line1 + line2 + line3 + exitLine
     ));
     String expectedOutput =
         INPUT_LINE_PREFIX + line1 +
         RESPONSE_PLACEHOLDER + "\n" +
         INPUT_LINE_PREFIX + line2 +
         RESPONSE_PLACEHOLDER + "\n" +
         INPUT_LINE_PREFIX + line3 +
         RESPONSE_PLACEHOLDER + "\n" +
         INPUT_LINE_PREFIX + exitLine +
         EXIT_RESPONSE + "\n"; 

     String[] ignoredCommandLineParams = null; 

     new TestableLoopingConsoleExample().main(ignoredCommandLineParams, in, new PrintStream(out)); 

     assertEquals(expectedOutput, out.toString());
  } 

}
查看更多
beautiful°
3楼-- · 2019-01-11 12:06

Why not write your application to take a Reader as input? That way, you can easily replace an InputStreamReader(System.in) with a FileReader(testFile)

public class Processor {
    void processInput(Reader r){ ... }
}

And then two instances:

Processor live = new Processor(new InputStreamReader(System.in));
Processor test = new Processor(new FileReader("C:/tmp/tests.txt");

Getting used to coding to an interface will bring great benefits in almost every aspect of your programs!

Note also that a Reader is the idiomatic way to process character-based input in Java programs. InputStreams should be reserved for raw byte-level processing.

查看更多
我只想做你的唯一
5楼-- · 2019-01-11 12:14

I suggest you to separate the code into three parts:

  • Read input (like name in your example)
  • Do what you need to do with that input
  • Print the results

You do not need to test reading input and printing results, as that's Java code that is already tested by people writing Java.

The only thing you need to test is the thing you are doing, whatever that is. Unit tests are named like that because they tests units of code in isolation. You don't test the whole program, you test small pieces that are self-contained and have a well-defined function.

In unit tests, you should not rely on input/output operations. You should provide inputs and expected outputs directly in the unit test. It is sometimes convenient to use File reading operations to supply input or output (e.g. if the amount of data is huge), but as a general rule the more you go into input/output in your unit tests, the more complex they become and you are more likely not to do unit, but integration tests.

In your case, you use name somehow. If that is the only parameter, then make a method - let's call it nameConsumer - that takes that name, does something and returns its result. In your unit tests, do something like this:

@Test
public void testNameConsumer() {
    // Prepare inputs
    String name = "Jon";
    String result = nameConsumer(name);
    assertEquals("Doe", result);
}

Move your println and readLine calls to other methods and use around nameConsumer, but not in your unit tests.

Read more about this here:

Keep it simple, it pays off.

查看更多
走好不送
6楼-- · 2019-01-11 12:18

The library System Rules provides the rule TextFromStandardInputStream for simulating input in JUnit tests.

public class YourAppTest {
  @Rule
  public TextFromStandardInputStream systemInMock = emptyStandardInputStream();

  @Test
  public void test() {
    systemInMock.provideText("name\nsomething else\n");
    YourApp.main();
    //assertSomething
  }
}

For details have a look at the System Rules documentation.

查看更多
登录 后发表回答