我正在尝试为需要用户输入的方法创建一些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;
}
是否有可能在JUnit测试方法中自动将程序传递给int而不是我或其他人手动执行此操作?像模拟用户输入一样?
提前致谢。
答案 0 :(得分:85)
您可以替换System.in with you own stream by calling System.setIn(InputStream in)。 输入流可以是字节数组:
ByteArrayInputStream in = new ByteArrayInputStream("My string".getBytes());
System.setIn(in);
// do your thing
// optionally, reset System.in to its original
System.setIn(System.in)
通过将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;
}
答案 1 :(得分:16)
要测试驱动代码,您应该为系统输入/输出功能创建一个包装器。你可以使用依赖注入来实现这一点,为我们提供一个可以请求新整数的类:
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();
}
}
然后你可以使用模拟框架(我使用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.");
}
然后编写通过测试的函数。该函数更加清晰,因为您可以删除询问/获取整数重复,并封装实际的系统调用。
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;
}
对于你的小例子来说,这似乎有些过分,但如果你正在构建一个更大的应用程序,那么这样的开发可以很快得到回报。
答案 2 :(得分:5)
测试类似代码的一种常用方法是提取一个接收Scanner和PrintWriter的方法,类似于this 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;
}
请注意,在结束之前您将无法读取输出,并且您必须预先指定所有输入:
@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.")););
}
当然,您可以将“scanner”和“输出”保留为被测系统中的可变字段,而不是创建重载方法。我倾向于将课程视为尽可能无国籍,但如果对你或你的同事/导师来说这不是一个很大的让步。
您也可以选择将测试代码放在与测试代码相同的Java包中(即使它位于不同的源文件夹中),这样可以放宽对两个参数重载的可见性,使其成为包私有
答案 3 :(得分:2)
您可以首先从键盘中提取检索数字的逻辑到自己的方法。然后,您可以测试验证逻辑,而无需担心键盘。为了测试keyboard.nextInt()调用,您可能需要考虑使用模拟对象。
答案 4 :(得分:2)
我设法找到一种更简单的方法。但是,您必须使用外部库System.rules By @Stefan Birkner
我只是拿了那里提供的例子,我认为它不可能变得更简单:)
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());
}
}
然而问题是在我的情况下我的第二个输入有空格,这使整个输入流为空!
答案 5 :(得分:1)
我发现创建一个定义类似于java.io.Console的方法的接口很有用,然后用它来读取或写入System.out。真正的实现将委托给System.console(),而您的JUnit版本可以是具有固定输入和预期响应的模拟对象。
例如,您将构建一个包含来自用户的预设输入的MockConsole。每次调用readLine时,模拟实现都会从列表中弹出一个输入字符串。它还会收集写入响应列表的所有输出。在测试结束时,如果一切顺利,那么您的所有输入都将被读取,您可以在输出上断言。
答案 6 :(得分:1)
我已经解决了从stdin读取模拟控制台的问题......
我的问题是我想尝试在JUnit中编写测试控制台来创建某个对象...
问题就像你说的那样:我如何从JUnit测试中写入Stdin?
然后在大学时我学习重定向,就像你说System.setIn(InputStream)改变了stdin filedescriptor,然后你就可以写了...
但还有一个问题要修复...... JUnit测试块等待从你的新InputStream读取,所以你需要创建一个线程来读取InputStream和JUnit测试线程写入新的Stdin ...你必须在Stdin中写,因为如果你稍后写的创建线程从stdin读取你可能会有竞争条件...你可以在读取之前写入InputStream或者你可以在写入之前从InputStream读取... < / p>
这是我的代码,我的英语技能很差我希望你能理解这个问题以及从JUnit测试中模拟stdin写入的解决方案。
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();
}