我如何测试 CLI 应用程序是否等待输入

How might I test that a CLI app waits for input

这与这个问题不同:JUnit: How to simulate System.in testing?,这是关于模拟标准输入。

我想知道的是如何测试(如在 TDD 中一样)使用 main 方法的简单 Java class 等待输入。

我的测试:

@Test 
public void appRunShouldWaitForInput(){
    long startMillis = System.currentTimeMillis();
    // NB obviously you'd want to run this next line in a separate thread with some sort of timeout mechanism... 
    // that's an implementation detail I've omitted for the sake of avoiding clutter!
    App.main( null );
    long endMillis = System.currentTimeMillis();
    assertThat( endMillis - startMillis ).isGreaterThan( 1000L );
}

我的 SUT main:

public static void main(String args[]) {
    BufferedReader br = null;
    try {
        br = new BufferedReader(new InputStreamReader(System.in));
        System.out.print("Enter something : ");
        String input = br.readLine();
    } catch (IOException e) {
        e.printStackTrace();
    } 

...测试失败。代码不会等待。但是,当您 运行 在命令提示符下应用程序时,它确实会等待。

注意,顺便说一句,我也尝试过将 stdin 设置为 sthg else:

System.setIn(new ByteArrayInputStream( dummy.getBytes()));
scanner = new Scanner(System.in);

...这并没有阻止测试。

作为一个更普遍的规则,静态方法(例如 main 方法)很难测试。因此,您几乎从不从测试代码中调用 main 方法(或任何其他静态方法)。解决此问题的常见模式是将其转换为:

public class App {
    public static void main(String args[]) {
        BufferedReader br = null;
        try {
            br = new BufferedReader(new InputStreamReader(System.in));
            System.out.print("Enter something : ");
            String input = br.readLine();
        } catch (IOException e) {
            e.printStackTrace();
        } 
    }
}

对此:

public class App {
    private final InputStream input;
    private final OutputStream output;

    public App(InputStream input, OutputStream output) {
        this.input = input;
        this.output = output;
    }

    public static void main(String[] args) {
        new App(System.in, System.out).start();
    }

    public void start() {
        BufferedReader br = null;
        try {
            br = new BufferedReader(new InputStreamReader(input));
            output.print("Enter something : ");
            String nextInput = br.readLine();
        } catch (IOException e) {
            e.printStackTrace();
        } 
    }
}

现在你的测试变成这样:

@Test 
public void appRunShouldWaitForInput(){
    ByteArrayOutputStream output = new ByteArrayOutputStream();

    // As you have already noted, you would need to kick this off on another thread and use a blocking implementation of InputStream to test what you want to test.
    new App(new ByteArrayInputStream(), output).start();

    assertThat(output.toByteArray().length, is(0));
}

关键思想是,当您 运行 应用 "for real" 即通过 main 方法时,它将使用标准输入和输出流。但是,当您从测试中 运行 它时,它使用纯内存 input/output 流,您可以在测试中完全控制它。 ByteArrayOutputStream 只是一个示例,但您可以在我的示例中看到该测试能够检查已写入输出流的实际字节数。

总的来说:您无法测试您的程序是否正在等待直到发生某些事情(因为您无法测试它是否永远等待)

遇到这种情况通常怎么办:

  1. 在你的情况下:不要测试它。 readLine 由经过大量测试的外部库提供。测试成本远高于此类测试的价值。重构和测试您的业务代码(string/stream 操作),而不是基础设施(系统输入)

  2. 在一般情况下,它正在测试并发编程。这很难,所以人们通常会尝试找到一些有用的简化:

    1. 您可以只测试您的程序输出对于测试中提供的输入是否正确。
    2. 对于一些非常困难的问题,上述技术与 运行 测试数千次(并在可能的情况下强制线程切换)相结合,以检测并发编程中的错误(违反不变量)
    3. 在您的测试提供输入日期之前,您可以测试您的程序是否处于正确的状态(等待线程、正确的字段值等)
    4. 在最坏的情况下,您可以在提供输入之前在测试中使用延迟,以确保您的程序等待并使用输入。这种技术经常被使用,因为它很简单,但它很臭,如果你添加更多这样的测试,你的整个套装会变慢