用模拟用户input进行JUnittesting

我正在尝试为需要用户input的方法创build一些JUnittesting。 被testing的方法看起来有点像下面的方法:

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; } 

有没有一种可能的方式来自动传递一个int而不是我或其他人在JUnittesting方法中手动执行此操作? 就像模拟用户input一样?

提前致谢。

您可以通过调用System.setIn(InputStream in)来将System.inreplace为您自己的stream 。 inputstream可以是字节数组:

 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作为参数使这个方法更可testing:

 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; } 

为了testing驱动你的代码,你应该为系统input/输出函数创build一个包装器。 你可以使用dependency injection来做到这一点,给我们一个可以要求新的整数的类:

 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)为您的函数创buildtesting:

 @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."); } 

然后编写通过testing的函数。 因为你可以删除询问/获取整数重复,并且实际的系统调用被封装,所以该函数更清晰。

 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; } 

对于你的小例子来说,这可能看起来过于矫枉过正了,但是如果你正在构build一个像这样开发的更大的应用程序,那么这个应用程序可以很快就收益。

testing类似代码的一个常见方法是提取一个方法,它接受一个Scanner和一个PrintWriter,类似于这个StackOverflow答案 ,然后testing:

 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; } 

请注意,直到最后,您将无法读取输出,并且必须事先指定所有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."));); } 

当然,不是创build一个重载方法,而是可以将“扫描器”和“输出”作为被测系统中的可变字段。 我倾向于尽可能地让class级保持无国籍状态,但是如果你或你的同事/指导老师对你很重要,那不是一个很大的让步。

您也可以select将testing代码放在与testing代码相同的Java包中(即使它位于不同的源文件夹中),这样可以将两个参数过载的可见性放到包私有的位置。

您可能首先将从键盘检索数字的逻辑抽出到自己的方法中。 然后你可以testingvalidation逻辑,而不必担心键盘。 为了testingkeyboard.nextInt()调用,你可能要考虑使用一个模拟对象。

我发现创build一个定义类似于java.io.Console的方法的接口是有帮助的,然后使用它来读取或写入System.out。 真正的实现将委托给System.console(),而您的JUnit版本可以是一个模拟对象,具有预设的input和预期的响应。

例如,您将构build一个包含来自用户的固定input的MockConsole。 每次调用readLine时,模拟实现将从列表中popupinputstring。 它也会把所有的输出都收集到一个回应列表中。 在testing结束时,如果一切顺利,那么您的所有input都将被读取,您可以在输出中声明。

我设法find一个更简单的方法。 但是,您必须使用@Stefan Birkner使用外部库System.rules

我只是提供了那里提供的例子,我认为它不能变得更简单:)

 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()); } } 

但是在我的情况下,我的第二个input的问题有空格,这使得整个inputstream为空!

我已经解决了从标准input读取模拟控制台的问题…

我的问题是我想尝试写在JUnittesting控制台创build一个特定的对象…

问题就像你所说的:我如何从JUnittesting中写Stdin?

然后在大学我学习redirect就像你说System.setIn(InputStream)改变标准input文件描述符,然后你可以写入…

但是还有一个问题需要解决… JUnittesting块等待您的新InputStream读取,因此您需要创build一个线程从InputStream中读取并从JUnittesting线程写入新的Stdin中…首先,您必须写在标准input,因为如果你以后写的创build线程从标准input读取,你可能会有竞争条件…你可以写在inputstream之前读取或您可以从InputStream之前读取之前写…

这是我的代码,我的英语技能不好,我希望你能理解这个问题,并从JUnittesting模拟stdin写入的解决scheme。

 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(); }