2

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

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

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()andSystem.setIn()方法,但我无法找到使该System.setIn()方法在这种情况下工作的方法,因为该reader.read()方法阻塞了我的线程。

我的测试设计错了吗?有没有办法设计sendInput()通过阻塞 reader.read() 调用工作的方法?

4

3 回答 3

6

我建议重构代码以允许注入输入/输出流,然后您可以模拟它们。如果你能把它改成类似

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. 它将逻辑从您的主类中取出。通常 main 方法实际上只是用于启动应用程序。

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

编辑:更新此重构如何帮助解决阻塞 IO 问题

通过使阅读器/输出可注入如上所示,您实际上不需要使用真正的 System.in 和 System.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();
}
于 2013-02-25T23:02:43.383 回答
1

我会重构 Main 以便更容易测试.. 像这样:

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

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

测试 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());
    }
}
于 2013-02-25T23:06:13.470 回答
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);
}

优点

  • 无需重构
  • 实际测试实现(无模拟/注入)
  • 不需要任何外部库

缺点

  • 当事情不能正常工作时很难调试(可修复)
  • 严重依赖操作系统行为(此类中的 Windows,但可修复)
  • 为每个测试类编译应用程序(我认为是可修复的?)
  • 出现错误且进程未被终止时的“内存泄漏”(我认为可修复?)

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

于 2013-03-07T06:57:10.563 回答