【发布时间】:2010-12-11 11:58:54
【问题描述】:
我有一个 Java 命令行程序。我想创建 JUnit 测试用例来模拟System.in。因为当我的程序运行时,它会进入 while 循环并等待用户输入。如何在 JUnit 中进行模拟?
谢谢
【问题讨论】:
标签: java command-line junit
我有一个 Java 命令行程序。我想创建 JUnit 测试用例来模拟System.in。因为当我的程序运行时,它会进入 while 循环并等待用户输入。如何在 JUnit 中进行模拟?
谢谢
【问题讨论】:
标签: java command-line junit
在技术上可以切换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);
}
【讨论】:
基于@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() 时,它将按照帮助方法出现的顺序调用它们:
setUpOutput(),因为 @Before 注释provideInput(String data),来自testCase1()
getOutput(),来自testCase1()
restoreSystemInputOutput(),因为 @After 注释我没有测试System.err,因为我不需要它,但它应该很容易实现,类似于测试System.out。
【讨论】:
String 传递给provideInput(String data) 就足够了。对于我在命令行界面上运行的程序,每次用户输入一些文本并点击Enter 时,该文本都会被视为新输入。请注意 SimpleProgram 使用 Java SE Scanner 类来读取用户的输入。
provideInput("Input 1\nInput 2\nInput 3");。
有几种方法可以解决这个问题。最完整的方法是在运行被测类时传入一个 InputStream,这是一个假 InputStream,它将模拟数据传递给你的类。如果您需要在代码中执行很多操作,可以查看依赖注入框架(例如 Google Guice),但简单的方法是:
public class MyClass {
private InputStream systemIn;
public MyClass() {
this(System.in);
}
public MyClass(InputStream in) {
systemIn = in;
}
}
在测试中,您将调用接受输入流的构造函数。您甚至可以将构造函数包私有化并将测试放在同一个包中,这样其他代码通常不会考虑使用它。
【讨论】:
InputData 作为单元测试中 InputStream 的高级包装器,你应该更关心你的类做了什么,而不是真正关心集成。
System.setIn(in),而不是创建变量systemIn。因此,您可以正常拨打System.in,但使用更新版本。
尝试重构您的代码以使用dependency injection。不要让您的方法直接使用System.in,而是让该方法接受InputStream 作为参数。然后在您的 junit 测试中,您将能够通过测试 InputStream 实现来代替 System.in。
【讨论】:
您可以使用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());
}
}
完全披露:我是该库的作者。
【讨论】:
TextFromStandardInputStream,但SystemOutRule 工作正常。
您可以创建一个自定义 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 );
虽然我认为你应该更好地测试你的类应该如何处理从输入流中读取的数据,而不是真正如何从那里读取。
【讨论】:
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”类中需要输入行的方法之前。
【讨论】:
可能是这样(未测试):
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);
【讨论】: