Java 使用模拟用户输入进行 JUnit 测试
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/6415728/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
JUnit testing with simulated user input
提问by Wimpey
I am trying to create some JUnit tests for a method that requires user input. The method under test looks somewhat like the following method:
我正在尝试为需要用户输入的方法创建一些 JUnit 测试。被测方法看起来有点像下面的方法:
public static int testUserInput() {
Scanner keyboard = new Scanner(System.in);
System.out.println("Give a number between 1 and 10");
int input = keyboard.nextInt();
while (input < 1 || input > 10) {
System.out.println("Wrong number, try again.");
input = keyboard.nextInt();
}
return input;
}
Is there a possible way to automatically pass the program an int instead of me or someone else doing this manually in the JUnit test method? Like simulating the user input?
有没有可能的方法来自动传递程序一个 int 而不是我或其他人在 JUnit 测试方法中手动执行此操作?喜欢模拟用户输入?
Thanks in advance.
提前致谢。
采纳答案by KrzyH
You can replace System.in with you own stream by calling System.setIn(InputStream in). InputStream can be a byte array:
您可以通过调用 System.setIn(InputStream in)将System.in替换为您自己的流。InputStream 可以是一个字节数组:
InputStream sysInBackup = System.in; // backup System.in to restore it later
ByteArrayInputStream in = new ByteArrayInputStream("My string".getBytes());
System.setIn(in);
// do your thing
// optionally, reset System.in to its original
System.setIn(sysInBackup);
Different approach can be make this method more testable by passing IN and OUT as parameters:
通过将 IN 和 OUT 作为参数传递,不同的方法可以使此方法更具可测试性:
public static int testUserInput(InputStream in,PrintStream out) {
Scanner keyboard = new Scanner(in);
out.println("Give a number between 1 and 10");
int input = keyboard.nextInt();
while (input < 1 || input > 10) {
out.println("Wrong number, try again.");
input = keyboard.nextInt();
}
return input;
}
回答by Sarah Haskins
You might start by extracting out the logic that retrieves the number from the keyboard into its own method. Then you can test the validation logic without worrying about the keyboard. In order to test the keyboard.nextInt() call you may want to consider using a mock object.
您可以首先提取从键盘检索数字的逻辑到它自己的方法中。然后你可以测试验证逻辑而不必担心键盘。为了测试 keyboard.nextInt() 调用,您可能需要考虑使用模拟对象。
回答by massfords
I've found it helpful to create an interface that defines methods similar to java.io.Console and then use that for reading or writing to the System.out. The real implementation will delegate to System.console() while your JUnit version can be a mock object with canned input and expected responses.
我发现创建一个接口来定义类似于 java.io.Console 的方法,然后使用它来读取或写入 System.out 很有帮助。真正的实现将委托给 System.console() 而您的 JUnit 版本可以是一个带有固定输入和预期响应的模拟对象。
For example, you'd construct a MockConsole that contained the canned input from the user. The mock implementation would pop an input string off the list each time readLine was called. It would also gather all of the output written to a list of responses. At the end of the test, if all went well, then all of your input would have been read and you can assert on the output.
例如,您将构建一个包含来自用户的预设输入的 MockConsole。每次调用 readLine 时,模拟实现都会从列表中弹出一个输入字符串。它还将收集写入响应列表的所有输出。在测试结束时,如果一切顺利,那么您的所有输入都将被读取,您可以对输出进行断言。
回答by Garrett Hall
To test drive your code, you should create a wrapper for system input/output functions. You can do this using dependency injection, giving us a class that can ask for new integers:
要测试驱动您的代码,您应该为系统输入/输出函数创建一个包装器。你可以使用依赖注入来做到这一点,给我们一个可以请求新整数的类:
public static class IntegerAsker {
private final Scanner scanner;
private final PrintStream out;
public IntegerAsker(InputStream in, PrintStream out) {
scanner = new Scanner(in);
this.out = out;
}
public int ask(String message) {
out.println(message);
return scanner.nextInt();
}
}
Then you can create tests for your function, using a mock framework (I use Mockito):
然后,您可以使用模拟框架(我使用 Mockito)为您的函数创建测试:
@Test
public void getsIntegerWhenWithinBoundsOfOneToTen() throws Exception {
IntegerAsker asker = mock(IntegerAsker.class);
when(asker.ask(anyString())).thenReturn(3);
assertEquals(getBoundIntegerFromUser(asker), 3);
}
@Test
public void asksForNewIntegerWhenOutsideBoundsOfOneToTen() throws Exception {
IntegerAsker asker = mock(IntegerAsker.class);
when(asker.ask("Give a number between 1 and 10")).thenReturn(99);
when(asker.ask("Wrong number, try again.")).thenReturn(3);
getBoundIntegerFromUser(asker);
verify(asker).ask("Wrong number, try again.");
}
Then write your function that passes the tests. The function is much cleaner since you can remove the asking/getting integer duplication and the actual system calls are encapsulated.
然后编写通过测试的函数。该函数更简洁,因为您可以删除询问/获取整数重复,并且封装了实际的系统调用。
public static void main(String[] args) {
getBoundIntegerFromUser(new IntegerAsker(System.in, System.out));
}
public static int getBoundIntegerFromUser(IntegerAsker asker) {
int input = asker.ask("Give a number between 1 and 10");
while (input < 1 || input > 10)
input = asker.ask("Wrong number, try again.");
return input;
}
This may seem like overkill for your small example, but if you are building a larger application developing like this can payoff rather quickly.
对于您的小示例来说,这似乎有点过分,但是如果您正在构建一个更大的应用程序,这样开发可以很快获得回报。
回答by Jeff Bowman
One common way to test similar code would be to extract a method that takes in a Scanner and a PrintWriter, similar to this StackOverflow answer, and test that:
测试类似代码的一种常见方法是提取一个接受 Scanner 和 PrintWriter 的方法,类似于这个 StackOverflow answer,并测试:
public void processUserInput() {
processUserInput(new Scanner(System.in), System.out);
}
/** For testing. Package-private if possible. */
public void processUserInput(Scanner scanner, PrintWriter output) {
output.println("Give a number between 1 and 10");
int input = scanner.nextInt();
while (input < 1 || input > 10) {
output.println("Wrong number, try again.");
input = scanner.nextInt();
}
return input;
}
Do note that you won't be able to read your output until the end, and you'll have to specify all of your input up front:
请注意,直到最后您才能读取输出,并且您必须预先指定所有输入:
@Test
public void shouldProcessUserInput() {
StringWriter output = new StringWriter();
String input = "11\n" // "Wrong number, try again."
+ "10\n";
assertEquals(10, systemUnderTest.processUserInput(
new Scanner(input), new PrintWriter(output)));
assertThat(output.toString(), contains("Wrong number, try again.")););
}
Of course, rather than creating an overload method, you could also keep the "scanner" and "output" as mutable fields in your system under test. I tend to like keeping classes as stateless as possible, but that's not a very big concession if it matters to you or your coworkers/instructor.
当然,除了创建重载方法之外,您还可以将“扫描仪”和“输出”作为被测系统中的可变字段。我倾向于尽可能让类保持无状态,但如果这对您或您的同事/讲师很重要,这并不是一个很大的让步。
You might also choose to put your test code in the same Java package as the code under test (even if it's in a different source folder), which allows you to relax the visibility of the two parameter overload to be package-private.
您还可以选择将您的测试代码与被测代码放在同一个 Java 包中(即使它位于不同的源文件夹中),这允许您将两个参数重载的可见性放宽为包私有。
回答by Omar Elshal
I managed to find a simpler way. However, you have to use external library System.rulesby @Stefan Birkner
我设法找到了一种更简单的方法。但是,你必须使用外部库System.rules通过@Stefan Birkner
I just took the example provided there, I think it couldn't have gotten more simpler:
我只是拿了那里提供的例子,我认为它不可能变得更简单:
import java.util.Scanner;
public class Summarize {
public static int sumOfNumbersFromSystemIn() {
Scanner scanner = new Scanner(System.in);
int firstSummand = scanner.nextInt();
int secondSummand = scanner.nextInt();
return firstSummand + secondSummand;
}
}
Test
测试
import static org.junit.Assert.*;
import static org.junit.contrib.java.lang.system.TextFromStandardInputStream.*;
import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.TextFromStandardInputStream;
public class SummarizeTest {
@Rule
public final TextFromStandardInputStream systemInMock
= emptyStandardInputStream();
@Test
public void summarizesTwoNumbers() {
systemInMock.provideLines("1", "2");
assertEquals(3, Summarize.sumOfNumbersFromSystemIn());
}
}
The problem however in my case my second input has spaces and this makes the whole input stream null!
然而,在我的情况下,问题是我的第二个输入有空格,这使得整个输入流为空!
回答by Javier Gutiérrez-Maturana Sánc
I have fixed the problem about read from stdin to simulate a console...
我已经解决了从标准输入读取模拟控制台的问题......
My problems was I'd like try write in JUnit test the console to create a certain object...
我的问题是我想尝试在 JUnit 中编写测试控制台以创建某个对象...
The problem is like all you say : How Can I write in the Stdin from JUnit test?
问题就像你说的一样:我如何从 JUnit 测试中写入 Stdin?
Then at college I learn about redirections like you say System.setIn(InputStream) change the stdin filedescriptor and you can write in then...
然后在大学我学习重定向就像你说 System.setIn(InputStream) 改变标准输入文件描述符,然后你可以写......
But there is one more proble to fix... the JUnit test block waiting read from your new InputStream, so you need create a thread to read from the InputStream and from JUnit test Thread write in the new Stdin... First you have to write in the Stdin because if you write later of create the Thread to read from stdin you likely will have race Conditions... you can write in the InputStream before to read or you can read from InputStream before write...
但是还有一个问题需要解决…… JUnit 测试块正在等待从您的新 InputStream 读取,因此您需要创建一个线程来从 InputStream 读取并从 JUnit 测试线程写入新的 Stdin ……首先您必须在标准输入中写入,因为如果您稍后编写创建线程以从标准输入读取,您可能会遇到竞争条件...
This is my code, my english skill is bad I hope all you can understand the problem and the solution to simulate write in stdin from JUnit test.
这是我的代码,我的英语水平很差我希望你能理解这个问题以及从 JUnit 测试模拟写入标准输入的解决方案。
private void readFromConsole(String data) throws InterruptedException {
System.setIn(new ByteArrayInputStream(data.getBytes()));
Thread rC = new Thread() {
@Override
public void run() {
study = new Study();
study.read(System.in);
}
};
rC.start();
rC.join();
}