JUnit: How to simulate System.in testing?

匿名 (未验证) 提交于 2019-12-03 00:44:02

问题:

I have a Java command-line program. I would like to create JUnit test case to be able to simulate System.in. Because when my program runs it will get into the while loop and waits for input from users. How do I simulate that in JUnit?

Thanks

回答1:

It is technically possible to switch System.in, but in general, it would be more robust not to call it directly in your code, but add a layer of indirection so the input source is controlled from one point in your application. Exactly how you do that is an implementation detail - the suggestions of dependency injection are fine, but you don't necessarily need to introduce 3rd party frameworks; you could pass round an I/O context from the calling code, for example.

How to switch System.in:

String data = "Hello, World!\r\n"; InputStream stdin = System.in; try {   System.setIn(new ByteArrayInputStream(data.getBytes()));   Scanner scanner = new Scanner(System.in);   System.out.println(scanner.nextLine()); } finally {   System.setIn(stdin); } 


回答2:

There are a few ways to approach this. The most complete way is to pass in an InputStream while running the class under test which is a fake InputStream which passes simulated data to your class. You can look at a dependency injection framework (such as Google Guice) if you need to do this a lot in your code, but the simple way is:

 public class MyClass {      private InputStream systemIn;       public MyClass() {          this(System.in);      }       public MyClass(InputStream in) {          systemIn = in;      }  } 

Under test you would call the constructor that takes the input stream. You cloud even make that constructor package private and put the test in the same package, so that other code would not generally consider using it.



回答3:

Try to refactor your code to use dependency injection. Instead of having your a method that uses System.in directly, have the method accept an InputStream as an argument. Then in your junit test, you'll be able to pass a test InputStream implementation in place of System.in.



回答4:

You can write a clear test for the command line interface by using the TextFromStandardInputStream rule of the System Rules library.

public void MyTest {   @Rule   public final TextFromStandardInputStream systemInMock     = emptyStandardInputStream();    @Test   public void readTextFromStandardInputStream() {     systemInMock.provideLines("foo");     Scanner scanner = new Scanner(System.in);     assertEquals("foo", scanner.nextLine());   } } 

Full disclosure: I'm the author of that library.



回答5:

You could create a custom InputStream and attach it to the System class

class FakeInputStream extends InputStream {      public int read() {          return -1;     } } 

And then use it with your Scanner

System.in = new FakeInputStream();

Before:

InputStream in = System.in; ... Scanner scanner = new Scanner( in ); 

After:

InputStream in = new FakeInputStream(); ... Scanner scanner = new Scanner( in ); 

Although I think you should better to test how your class should work with the data read from the input stream and not really how it reads from there.



回答6:

The problem with BufferedReader.readLine() is that it is a blocking method which waits for user input. It seems to me that you don't particularly want to simulate that (i.e. you want tests to be fast). But in a testing context it continually returns null at high speed during testing, which is irksome.

For a purist you can make the getInputLine below package-private, and mock it: easy-peezy.

String getInputLine() throws Exception {     return br.readLine(); } 

... you'd have to make sure that you had a way of stopping (typically) a loop of user interaction with the app. You'd also have to cope with the fact that your "input lines" would always be the same until you somehow changed the doReturn of your mock: hardly typical of user input.

For a non-purist who wishes to make life easy for themselves (and produce readable tests) you could put all this stuff below in your app code:

private Deque inputLinesDeque;  void setInputLines(List inputLines) {     inputLinesDeque = new ArrayDeque(inputLines); }  private String getInputLine() throws Exception {     if (inputLinesDeque == null) {         // ... i.e. normal case, during app run: this is then a blocking method         return br.readLine();     }     String nextLine = null;     try {         nextLine = inputLinesDeque.pop();     } catch (NoSuchElementException e) {         // when the Deque runs dry the line returned is a "poison pill",          // signalling to the caller method that the input is finished         return "q";     }      return nextLine; } 

... in your test you might then go like this:

consoleHandler.setInputLines( Arrays.asList( new String[]{ "first input line", "second input line" })); 

before triggering off the method in this "ConsoleHandler" class which needs input lines.



回答7:

maybe like this (not tested):

InputStream save_in=System.in;final PipedOutputStream in = new PipedOutputStream(); System.setIn(new PipedInputStream(in));  in.write("text".getBytes("utf-8"));  System.setIn( save_in ); 

more parts:

//PrintStream save_out=System.out;final ByteArrayOutputStream out = new ByteArrayOutputStream();System.setOut(new PrintStream(out));  InputStream save_in=System.in;final PipedOutputStream in = new PipedOutputStream(); System.setIn(new PipedInputStream(in));  //start something that reads stdin probably in a new thread //  Thread thread=new Thread(new Runnable() { //      @Override //      public void run() { //          CoursesApiApp.main(new String[]{});                  //      } //  }); //  thread.start();   //maybe wait or read the output //  for(int limit=0; limit


标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!