【问题标题】:How can one JUnit test an interactive, text-based Java application?JUnit 如何测试基于文本的交互式 Java 应用程序?
【发布时间】:2014-01-22 22:36:56
【问题描述】:

理想情况下,我想编写 JUnit 测试代码,以交互方式测试学生基于文本的 I/O 应用程序。使用System.setIn()/.setOut() 会导致问题,因为底层流是阻塞的。 Birkner 的系统规则 (http://www.stefan-birkner.de/system-rules/index.html) 在较早的帖子 (Testing console based applications/programs - Java) 中被推荐,但它似乎要求在运行单元测试目标之前提供所有标准输入,因此不是交互式的。

要提供一个具体的测试目标示例,请考虑以下猜谜游戏代码:

public static void guessingGame() {
    Scanner scanner = new Scanner(System.in);
    Random random = new Random();
    int secret = random.nextInt(100) + 1;
    System.out.println("I'm thinking of a number from 1 to 100.");
    int guess = 0;
    while (guess != secret) {
        System.out.print("Your guess? ");
        guess = scanner.nextInt();
        final String[] responses = {"Higher.", "Correct!", "Lower."};
        System.out.println(responses[1 + new Integer(guess).compareTo(secret)]);
    }
}

现在想象一个 JUnit 测试,它将提供猜测、阅读响应并完成游戏。如何在 JUnit 测试框架中实现这一点?

回答:

使用下面Andrew Charneski推荐的方法,添加输出刷新(包括在上面每个打印语句后添加System.out.flush();),非随机播放,恢复System.in/out,这段代码似乎执行了我的测试正在想象:

@Test
public void guessingGameTest() {
    final InputStream consoleInput = System.in;
    final PrintStream consoleOutput = System.out;
    try {
        final PipedOutputStream testInput = new PipedOutputStream();
        final PipedOutputStream out = new PipedOutputStream();
        final PipedInputStream testOutput = new PipedInputStream(out);
        System.setIn(new PipedInputStream(testInput));
        System.setOut(new PrintStream(out));
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    PrintStream testPrint = new PrintStream(testInput);
                    BufferedReader testReader = new BufferedReader(
                            new InputStreamReader(testOutput));
                    assertEquals("I'm thinking of a number from 1 to 100.", testReader.readLine());
                    int low = 1, high = 100;
                    while (true) {
                        if (low > high)
                            fail(String.format("guessingGame: Feedback indicates a secret number > %d and < %d.", low, high));
                        int mid = (low + high) / 2;
                        testPrint.println(mid);
                        testPrint.flush();
                        System.err.println(mid);
                        String feedback = testReader.readLine();
                        if (feedback.equals("Your guess? Higher."))
                            low = mid + 1;
                        else if (feedback.equals("Your guess? Lower."))
                            high = mid - 1;
                        else if (feedback.equals("Your guess? Correct!"))
                            break;
                        else
                            fail("Unrecognized feedback: " + feedback);
                    }
                } catch (IOException e) {
                    e.printStackTrace(consoleOutput);
                } 
            }
        }).start();
        Sample.guessingGame();
    }
    catch (IOException e) {
        e.printStackTrace();
        fail(e.getMessage());
    }
    System.setIn(consoleInput);
    System.setOut(consoleOutput);
}

【问题讨论】:

  • 你的意思是像Sikuli这样的东西吗?
  • Sikuli 似乎面向图形用户界面,而我专注于基于文本的 I/O。不过,感谢您的链接。我会把它藏起来改天。

标签: java unit-testing junit


【解决方案1】:

使用 PipedInput/OutputStream,例如

    final PrintStream consoleOutput = System.out;
    final PipedOutputStream testInput = new PipedOutputStream();
    final PipedOutputStream out = new PipedOutputStream();
    final PipedInputStream testOutput = new PipedInputStream(out);
    System.setIn(new PipedInputStream(testInput));
    System.setOut(new PrintStream(out));
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                PrintStream testPrint = new PrintStream(testInput);
                BufferedReader testReader = new BufferedReader(
                    new InputStreamReader(testOutput));
                while (true) {
                    testPrint.println((int) (Math.random() * 100));
                    consoleOutput.println(testReader.readLine());
                }
            } catch (IOException e) {
                e.printStackTrace(consoleOutput);
            }
        }
    }).start();
    guessingGame();

【讨论】:

  • 这种方法适用于我的目的。我建议进行一项改进。在我的机器上,上述方法导致我的系统启动之间存在一秒钟的延迟。我发现在两端打印后调用 flush() 可以消除这些一秒的延迟。我将暂时使用这种方法添加非随机测试代码。
【解决方案2】:

最好的方法是将输入和游戏逻辑分开。

为输入部分创建一个接口(使用getNextGuess 之类的方法)和一个放置扫描仪的具体实现。这样您以后也可以扩展/交换它。然后在您的单元测试中,您可以模拟该类以提供您需要测试的输入。

【讨论】:

  • 我同意这将是大多数情况下的最佳方法。指定模型-视图-控制器 (link) 会有所帮助,但会给 CS 入门学生带来一些问题,这些问题距离“Hello, world!”仅几步之遥。并且对 O-O 还不满意。这是我正在寻求为其开发 JUnit 测试的区域。理想情况下,它们现在不能分离到 MVC 中,我可以通过更复杂的测试来弥补。
【解决方案3】:

也许您应该问自己要确保代码的哪些属性。 例如:您可能希望确保根据输入给出正确的响应。然后你应该重构你的代码并提取一个像

这样的函数
String getResponse(int secret, int guess) 
{
    ...
}

然后就可以测试了

AssertEquals("Higher.",getResponse(50,51));
AssertEquals("Correct!",getResponse(50,50));
AssertEquals("Lower.",getResponse(50,49));

测试包括随机数在内的完整流程没有多大意义。 您可能会制作一个 testloop 0..100,但最好测试下端/上端以及介于两者之间的东西。 而且您不需要在单元测试中进行交互式输入。没有意义。它只能更低、更高或相等。

【讨论】:

  • 虽然我会将作业规范分离为模型-视图-控制器抽象以供更高级的学生使用,但这里的测试目标是针对那些刚刚超出“Hello, world!”的学生,学习简单的 I/ O,控制流等,还没有深入O-O。一旦他们掌握了 O-O 基础知识,我同意这种关注点分离是明智的。
【解决方案4】:

我会将 System.in 和 System.out 包装在一个对象中,然后您可以将其注入。这样,您可以在您的 junit 测试中注入一个模拟。也将有利于交换您将来可能想要用于输入和输出的内容! :)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-11-23
    • 1970-01-01
    • 2012-05-06
    • 2011-05-12
    • 2014-02-11
    • 1970-01-01
    • 2011-04-07
    相关资源
    最近更新 更多