【问题标题】:JUnit: How to simulate System.in testing?JUnit:如何模拟 System.in 测试?
【发布时间】:2010-12-11 11:58:54
【问题描述】:

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

谢谢

【问题讨论】:

标签: java command-line junit


【解决方案1】:

在技术上可以切换System.in,但一般来说,不直接在代码中调用它会更健壮,而是添加一个间接层,以便从应用程序中的一个点控制输入源。究竟如何做到这一点是一个实现细节——依赖注入的建议很好,但你不一定需要引入 3rd 方框架;例如,您可以从调用代码中传递一个 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);
}

【讨论】:

    【解决方案2】:

    基于@McDowell's answer 和another answer that shows how to test 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。

    【讨论】:

    • 如何继续使用多个输入?
    • 对于我所需要的,将带有换行符的String 传递给provideInput(String data) 就足够了。对于我在命令行界面上运行的程序,每次用户输入一些文本并点击Enter 时,该文本都会被视为新输入。请注意 SimpleProgram 使用 Java SE Scanner 类来读取用户的输入。
    • 所以,我本可以打电话给provideInput("Input 1\nInput 2\nInput 3");。
    【解决方案3】:

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

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

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

    【讨论】:

    • +1。我和你在这件事上。我会更进一步:InputData 作为单元测试中 InputStream 的高级包装器,你应该更关心你的类做了什么,而不是真正关心集成。
    • 您可以使用方法System.setIn(in),而不是创建变量systemIn。因此,您可以正常拨打System.in,但使用更新版本。
    【解决方案4】:

    尝试重构您的代码以使用dependency injection。不要让您的方法直接使用System.in,而是让该方法接受InputStream 作为参数。然后在您的 junit 测试中,您将能够通过测试 InputStream 实现来代替 System.in。

    【讨论】:

      【解决方案5】:

      您可以使用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(); 一起使用,就我而言似乎,这两者无法结合......所以我无法将它与注入模拟结合......
      • 不应该这样。您能否为系统规则库创建一个问题并提供您失败的示例。
      • 谢谢...现在我知道它是有用的,如果适用,我会调查它并创建一个问题。这似乎是我遇到问题的TextFromStandardInputStream,但SystemOutRule 工作正常。
      • 这是一个很棒的库,但请注意它与 junit5 不兼容。我不得不回到junit4才能让它工作。这是一个已知的错误,请参阅 stefan 评论中链接的 git repo
      • @Daniel,现在有system-lambda,兼容junit5和测试。
      【解决方案6】:

      您可以创建一个自定义 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,从 test-after 的角度来看,这是一件非常合理的事情——利用系统全局。
      • 你不能只做 System.in = xxx 因为 System.in 是最终的。您可以使用 System.setIn,但请确保您在拆卸时返回默认值。此外,您不需要滚动自己的 InputStream,ByteArrayInputStream 会很好地完成这项工作。
      • 哦,是的,我很困惑。我想说的是......好吧,我将编辑我的条目:)
      【解决方案7】:

      BufferedReader.readLine() 的问题在于它是一种等待用户输入的阻塞方法。在我看来,您并不是特别想模拟它(即您希望测试速度快)。但在测试环境中,它会在测试期间不断高速返回null,这很烦人。

      对于纯粹主义者,您可以将包下面的 getInputLine 设为私有,并模拟它:easy-peezy。

      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”类中需要输入行的方法之前。

      【讨论】:

        【解决方案8】:

        可能是这样(未测试):

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多