2013-02-25 97 views
2

我被要求在从命令行运行和运行的遗留Java应用程序中引入单元测试。基本上主循环打印出一个菜单,用户输入内容并显示更多数据。JUnit围绕Sytem.in和System.out进行测试

这个主类说明了应用程序是如何工作的。

public class Main{ 

    static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); 

    public static void main(String argv[]) throws IOException{ 
     while (true) { 
      char input = (char) reader.read(); 

      if(input == 'x'){ 
       return; 
      } 

      System.out.println(input); 
     } 
    } 
} 

我想我的测试方法是这个样子

public void testCaseOne(){ 
    Main.main(); 
    String result = ""; 

    result = sendInput("1"); 
    assertEqual(result, "1"); 

    result = sendInput("x"); 
    assertEqual(result,""); 
} 

我知道的System.setOut()System.setIn()方法,但我不能想出一个办法,使System.setIn()方法工作这种情况下,因为reader.read()方法阻止我的线程。

我的测试设计是否错误? 有没有办法设计sendInput()方法来完成阻塞reader.read()调用?

+2

如果您希望您的代码在标准输入上进行侦听,同时也将测试代码写入标准输入,那么顺序代码将不够用。我相信你需要两个线程。一个用于Main.main(),另一个用于sendInput(“1”); – mjshaw 2013-02-25 22:05:43

+0

您是否正在测试reader.read()执行正确的操作,还是在传递'x'时while循环退出? – 2013-02-25 22:53:31

+1

我甚至会超越Threads,开始使用ProcessBuilder作为自己的进程测试应用程序。这样你就不需要担心重定向任何东西 – radai 2013-02-25 23:02:37

回答

6

我会建议重构代码以允许输入/输出流被注入,然后你可以嘲笑它们。如果你couuld它更改为类似

public class Main{ 

    static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); 

    public static void main(String argv[]) throws IOException{ 
     new YourClass(reader,System.out).run(); 
    } 
} 

public class YourClass { // I don't know what your class is actually doing, but name it something appropriate 
    private final InputReader reader; 
    private final PrintStream output; 

    public YourClass(InputReader reader, PrintStream output) { 
     this.reader = reader; 
     this.output = ouptut; 
    } 

    public void run() { 

     while (true) { 
     char input = (char) reader.read(); 

     if(input == 'x') 
      return; 

     output.println(input); 
    } 
} 

这样的设计做了几件事情:

  1. 它采用逻辑移出主类。通常,主要方法实际上仅用于启动应用程序。

  2. 它使YourClass更容易单元测试。在你的测试中,你可以简单地嘲笑输入/输出。

编辑:更新到这个重构与阻塞IO问题

如何有助于使读者/输出注射如上图所示,你实际上并不需要使用真正的System.in和系统.out - 你可以使用模拟代替。这消除了实际上具有阻止读取的需要。

public void testCaseOne(){ 
    // pseudocode for the mock - this will vary depending on your mock framework 
    InputReader reader = createMock(InputReader); 
    // the first time you read it will be a "1", the next time it will be an "x" 
    expect(reader.read()).andReturn("1"); 
    expect(reader.read()).andReturn("x"); 

    PrintStream stream = createMock(PrintStream); 
    // only expect the "1" to get written. the "x" is the exit signal 
    expect(stream.println("1")); 

    new YourClass(reader,stream).run(); 
    verifyMocks(); 
} 
+0

这与使用System.setIn()和System.setOut()方法有什么不同?我可以使用这些方法控制输入和输出,而不必重构10+个类,但是我似乎无法用reader.read()来覆盖阻塞的IORead ... – Alexandre 2013-02-26 02:37:27

+0

@PeekaySwitch查看我的编辑。如果你不熟悉mock,有很多很好的java模拟库,例如EasyMock,PowerMock和JMock。 – 2013-02-26 02:52:28

+1

恕我直言,这是一个嘲笑的滥用。在你的测试中使用一个真正的阅读器,如StringReader。尽管如此,重构代码的建议仍然是重点。 – NamshubWriter 2013-02-26 16:56:44

1

我会重构主营所以它更容易测试..像这样:

public class Main{ 

    private boolean quit = false; 

    public static void main(String[] argv) throws IOException { 
     Main main = new Main(); 
     BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); 
     char input = main.readInput(reader); 
     while (!main.quit()) { 
      System.out.println(input); 
      input = main.readInput(reader); 
     } 
    } 

    public char readInput(Reader reader) throws IOException{ 
     char input = (char) reader.read(); 

     if(input == 'x'){ 
      quit = true; 
      return '\0'; 
     } 

     return input; 
    } 

    public boolean quit(){ 
     return quit; 
    } 
} 

就个人而言,我尽量远离静态变量了。如果你需要一个,你可以在上面的主要方法中声明它。

测试while(true)几乎是不可能的,因为测试while循环从不退出将花费无限的时间。那么是否应该测试main.quit() == true案例中的循环退出是个问题。就个人而言,我只想测试核心逻辑,剩下的未经测试:

public class MainTest { 

    private Main main; 

    @Before 
    public void setup(){ 
     main = new Main(); 
    } 

    @Test 
    public void testCaseOne() throws IOException{ 

     char result1 = main.readInput(new StringReader("1")); 
     assertEquals(result1, '1'); 
     assertFalse(main.quit()); 

     char result2 = main.readInput(new StringReader("x")); 
     assertEquals(result2, '\0'); 
     assertTrue(main.quit()); 
    } 
} 
0

这里是我与去解决方案不需要对遗留代码的重构。

简而言之,我做了一个抽象测试类,它在一个单独的线程上编译和执行一个进程中的应用程序。我将自己附在进程的输入/输出上并对其进行读/写。

public abstract class AbstractTest extends TestCase{ 

    private Process process; 
    private BufferedReader input; 
    private BufferedWriter output; 

    public AbstractTest() { 
     //Makes a text file with all of my .java files for the Java Compiler process 
     Process pDir = new ProcessBuilder("cmd.exe", "/C", "dir /s /B *.java > sources.txt").start(); 
     pDir.waitFor(); 

     //Compiles the application 
     Process p = new ProcessBuilder("cmd.exe", "/C", "javac @sources.txt").start(); 
     p.waitFor(); 
    } 


    protected void start(){ 
     Thread thread = new Thread() { 
      public void run() { 
       //Execute the application 
       String command = "java -cp src/main packagename.Main "; 
       AbstractTest.this.process = = new ProcessBuilder("cmd.exe", "/C", command).start(); 
       AbstractTest.this.input = new BufferedReader(new InputStreamReader(AbstractTest.this.process.getInputStream())); 
       AbstractTest.this.output = new BufferedWriter(new OutputStreamWriter(AbstractTest.this.process.getOutputStream())); 
      } 
     } 
    } 

    protected String write(String data) { 
     output.write(data + "\n"); 
     output.flush(); 
     return read(); 
    } 

    protected String read(){ 
     //use input.read() and read until it makes senses 
    } 

    protected void tearDown() { 
     this.process.destroy(); 
     this.process.waitFor(); 
     this.input.close(); 
     this.output.close(); 
    } 

} 

之后,制作实际的测试类和实现真正的测试方法非常容易。

public void testOption3A(){ 
    start(); 
    String response = write("3"); 
    response = write("733"); 
    assertEquals("*** Cactus ID 733 not found ***",response); 
} 

优点

  • 没有重构需要
  • 实际测试的执行情况(无嘲讽/注射)
  • 不需要任何外部librairies

缺点

  • 相当困难的调试时,事情没有proprely(可修复的)
  • 严重依赖操作系统的行为(窗口在这个类,但可改正的)
  • 编译为每一个测试类(应用程序可固定工作的我觉得呢?)
  • “内存泄漏”时,有一个错误,并且进程没有中止 (可固定,我认为?)

这可能是一个临界“黑客”,但它满足了我的需求和要求。