JUnit:如何模拟System.in进行测试?

66

我有一个Java命令行程序。我想创建JUnit测试用例来模拟System.in。因为当我的程序运行时,它会进入while循环并等待用户输入。如何在JUnit中模拟这个过程?

谢谢


https://www.baeldung.com/java-testing-system-out-println - Ahmed Nabil
9个回答

71

从技术上讲,切换System.in是可行的,但通常来说,在代码中直接调用它可能不够健壮,最好添加一层间接层,以便从应用程序的一个点控制输入源。具体如何实现这一点是一个实现细节 - 依赖注入的建议很好,但您不一定需要引入第三方框架;例如,您可以从调用代码传递一个I/O上下文。

如何切换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);
}

27

基于@ McDowell的答案另一个展示如何测试System.out的答案,我想分享我的解决方案,以给一个程序输入并测试其输出。

作为参考,我使用JUnit 4.12。

假设我们有这个简单的程序,它只是将输入复制到输出:

import java.util.Scanner;

public class SimpleProgram {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.print(scanner.next());
        scanner.close();
    }
}

为了测试它,我们可以使用以下类:

import static org.junit.Assert.*;

import java.io.*;

import org.junit.*;

public class SimpleProgramTest {
    private final InputStream systemIn = System.in;
    private final PrintStream systemOut = System.out;

    private ByteArrayInputStream testIn;
    private ByteArrayOutputStream testOut;

    @Before
    public void setUpOutput() {
        testOut = new ByteArrayOutputStream();
        System.setOut(new PrintStream(testOut));
    }

    private void provideInput(String data) {
        testIn = new ByteArrayInputStream(data.getBytes());
        System.setIn(testIn);
    }

    private String getOutput() {
        return testOut.toString();
    }

    @After
    public void restoreSystemInputOutput() {
        System.setIn(systemIn);
        System.setOut(systemOut);
    }

    @Test
    public void testCase1() {
        final String testString = "Hello!";
        provideInput(testString);

        SimpleProgram.main(new String[0]);

        assertEquals(testString, getOutput());
    }
}

我不会解释太多,因为我相信代码是可读的,并且我引用了我的来源。

当JUnit运行testCase1()时,它将按照它们出现的顺序调用辅助方法:

  1. setUpOutput(),由于@Before注释
  2. provideInput(String data),从testCase1()调用
  3. getOutput(),从testCase1()调用
  4. restoreSystemInputOutput(),由于@After注释

我没有测试System.err,因为我不需要它,但实现类似于测试System.out应该很容易。


3
如果有多个输入,您会如何继续进行? - Moustachio
2
对于我所需的内容,只需将带有换行符的String传递给provideInput(String data)即可。对于我的程序来说,它运行在命令行界面上,每当用户输入一些文本并按下“Enter”键时,该文本就被视为新的输入。请注意,SimpleProgram使用Java SE Scanner类来读取用户的输入。 - Antônio Medeiros
3
所以,我本来可以调用 provideInput("Input 1\nInput 2\nInput 3"); - Antônio Medeiros

8

有几种方法可以解决这个问题。最完整的方法是在运行测试类时传入一个InputStream,这是一个伪造的InputStream,它将模拟数据传递给你的类。如果您需要在代码中经常执行此操作,则可以查看依赖注入框架(例如Google Guice),但简单的方法是:

 public class MyClass {
     private InputStream systemIn;

     public MyClass() {
         this(System.in);
     }

     public MyClass(InputStream in) {
         systemIn = in;
     }
 }

在测试中,您将调用接受输入流的构造函数。您甚至可以将该构造函数设置为包私有并将测试放置在同一包中,以便其他代码通常不会考虑使用它。

3
+1。我完全同意你的观点。我认为我们可以更进一步:在单元测试中,将InputData作为对InputStream的高级封装,这样你就可以更关注你的类所做的事情,而不是集成方面的问题。 - OscarRyz
1
你可以使用方法 System.setIn(in) 来代替创建变量 systemIn。这样,你就可以正常调用 System.in,但是使用的是更新后的版本。 - Laraconda

6
尝试重构您的代码以使用依赖注入。不要直接使用System.in的方法,而是将该方法作为参数接受InputStream。然后在您的JUnit测试中,您可以传递一个测试InputStream实现来替换System.in

4
您可以使用System Rules库的TextFromStandardInputStream规则编写清晰的命令行界面测试。
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());
  }
}

完全披露:我是那个库的作者。

我刚刚尝试使用了你的库,它看起来非常不错...但是当我尝试使用Rule public MockitoRule rule = MockitoJUnit.rule();时,似乎无法将两者结合在一起...所以我无法将其与注入模拟对象相结合... - mike rodent
不应该出现这种情况。你能否为 System Rules 库创建一个问题,并提供你的失败示例呢? - Stefan Birkner
谢谢,现在我知道它应该如何工作了,如果适用的话,我会研究并创建一个问题。似乎是我遇到了TextFromStandardInputStream的问题,但SystemOutRule却可以正常工作。 - mike rodent
这是一个很棒的库,但请注意它与junit5不兼容。我不得不回到junit4才能让它工作。这是一个已知的bug,请参见Stefan评论中链接的git存储库。 - Daniel
@Daniel,现在有一个名为system-lambda的系统,它与junit5和测试兼容。 - MiB

1
你可以创建一个自定义的InputStream,并将其附加到System类。
class FakeInputStream extends InputStream {

    public int read() {
         return -1;
    }
}

然后与您的Scanner一起使用它

System.in = new FakeInputStream();

之前:

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

之后:

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

虽然我认为你最好测试一下你的类如何处理从输入流中读取的数据,而不是它如何从那里读取。


从TDD的角度来看,这避免了测试“驱动”或试图指示设计的情况。然而,OP没有指定TDD,从测试后的角度来看,这是一个非常合理的做法 - 利用系统全局变量。 - Yishai
你不能仅仅使用“System.in = xxx”来赋值,因为“System.in”是常量。你可以使用“System.setIn”,但一定要在拆除(tear down)时返回默认值。另外,你不需要编写自己的InputStream,ByteArrayInputStream会很好地完成任务。 - Michael Lloyd Lee mlk
哦,是的,我搞混了。我想说的是...好吧,我会编辑我的输入 :) - OscarRyz

1
< p > BufferedReader.readLine() 的问题在于它是一种阻塞方法,需要等待用户输入。在测试环境中,它会以高速连续返回null,这很让人恼火。看起来你不想模拟这种情况(即你希望测试速度快)。但对于纯粹主义者,您可以将下面的getInputLine设置为包私有,并进行模拟:易如反掌。

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

你需要确保有一种停止用户与应用程序交互的方式,通常是循环。同时你还需要应对这样一个事实:在你以某种方式改变模拟的doReturn之前,“输入行”始终相同,这并不典型。
对于那些希望为自己简化生活(并生成可读性强的测试)的非纯洁主义者,可以将下面所有的东西放在你的应用程序代码中:
private Deque<String> inputLinesDeque;

void setInputLines(List<String> inputLines) {
    inputLinesDeque = new ArrayDeque<String>(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;
}

... 在你的测试中,你可以这样做:

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

在触发需要输入行的“ConsoleHandler”类中的方法之前。


0

@Stefan Birkner,谢谢!

  1. 修改Pom.xml文件
Ref:
https://stackoverflow.com/a/66127606/8317677
https://github.com/stefanbirkner/system-lambda/blob/master/pom.xml
https://github.com/stefanbirkner/system-lambda/blob/master/src/test/java/com/github/stefanbirkner/systemlambda/WithTextFromSystemInTest.java

  <properties>
      <system-lambda.version>1.2.1</system-lambda.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>com.github.stefanbirkner</groupId>
      <artifactId>system-lambda</artifactId>
      <version>${system-lambda.version}</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
  • 添加函数代码
  • import java.io.BufferedReader;
    import java.io.InputStreamReader;
    
    public class SimpleProgram003 {
        public static void main(String[] args) {
            try{
                String c;
                BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
                do{
                    c = in.readLine();
                    System.out.println(c);
                    String d = c;
                }while(!c.equals("q"));
            }catch(Exception e){
                System.out.println("catch Exception");
            }
        }
    }
    
    1. 添加测试代码
    import static com.github.stefanbirkner.systemlambda.SystemLambda.*;
    import static org.junit.Assert.*;
    
    import java.io.ByteArrayInputStream;
    import java.io.ByteArrayOutputStream;
    import java.io.InputStream;
    import java.io.PrintStream;
    
    import org.junit.After;
    import org.junit.Before;
    import org.junit.Test;
    
    /**
     * Unit test for simple App. JUnit 4.x.
     */
    public class SimpleProgram003Test {
        private final InputStream systemIn = System.in;
        private final PrintStream systemOut = System.out;
    
        private ByteArrayInputStream testIn;
        private ByteArrayOutputStream testOut;
    
        @Before
        public void setUpOutput() {
            testOut = new ByteArrayOutputStream();
            System.setOut(new PrintStream(testOut));
        }
    
        private void setInput(String data) {
            testIn = new ByteArrayInputStream(data.getBytes());
            System.setIn(testIn);
        }
    
        private String getOutput() {
            return testOut.toString();
        }
    
        @After
        public void restoreSystemInputOutput() {
            System.setIn(systemIn);
            System.setOut(systemOut);
        }
    
        @Test
        public void testCase1() {
            final String testString = "Hello 1\nq\n";
            setInput(testString);
    
            SimpleProgram003.main(new String[0]);
            // String a = getOutput();
            assertEquals("Hello 1\r\nq\r\n", getOutput());
        }
    
        @Test // Multiply inputs
        public void testCase2() throws Exception {
            withTextFromSystemIn(
                "Input1",
                "Input2",
                "q",
                "Input3"
            ).execute(() -> {
                SimpleProgram003.main(new String[0]);
                // String a = getOutput();
                assertEquals("Input1\r\nInput2\r\nq\r\n", getOutput());
            });
        }
    }
    

    0

    可能是这样的(未经测试):

    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 );
    

    更多部分:

    //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<60 && not_ready ; limit++)
    //  {
    //      try {
    //          Thread.sleep(100);
    //      } catch (InterruptedException e) {
    //          e.printStackTrace();
    //      }
    //  }
    
    
    in.write("text".getBytes("utf-8"));
    
    System.setIn( save_in );
    
    //System.setOut(save_out);
    

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接