具有模拟用户输入的JUnit测试


81

我正在尝试为需要用户输入的方法创建一些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;
}

有没有一种方法可以自动将程序传递给int而不是我或其他人在JUnit测试方法中手动执行此操作?像模拟用户输入一样?

提前致谢。


7
您到底要测试什么?扫描器?测试方法通常应断言某些有用的东西。
Markus

2
您不必测试Java的Scanner类。您可以手动设置输入,只需测试自己的逻辑即可。int输入= -1或5或11将覆盖您的逻辑
blong824 2011年

3
六年后...这仍然是一个很好的问题。尤其是因为开始开发应用程序时,您通常可能不希望拥有JavaFX的所有花哨功能,而只是开始使用不起眼的命令行进行一些非常基本的交互。遗憾的是,JUnit并没有使它变得容易得多。对我来说,Omar Elshal的回答非常好,只涉及了最少的“人为”或“失真”应用程序编码...
mike rodent

Answers:


103

您可以通过调用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);

通过将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;
}

5
@KrzyH我将如何使用多个输入来执行此操作?假设在//做您的事情时,我有三个提示供用户输入。我该怎么办?
2014年

1
@ Stupid.Fat.Cat我添加了第二种解决问题的方法,更加优雅和
易懂

1
@KrzyH对于第二种方法,您将需要将输入流和输出流作为方法定义中的参数传递,这不是我想要的。您知道更好的方法吗?
希拉格2015年

6
如何模拟按Enter键?现在,该程序只读取一次所有的输入,这是不好的。我尝试过,\n但这没什么不同
CodyBugstein 2015年

3
@CodyBugsteinByteArrayInputStream in = new ByteArrayInputStream(("1" + System.lineSeparator() + "2").getBytes());用于获取不同keyboard.nextLine()呼叫的多个输入。
一罐粘土

18

为了测试代码,您应该为系统输入/输出功能创建一个包装器。您可以使用依赖项注入来做到这一点,为我们提供一个可以请求新整数的类:

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

对于您的小示例来说,这似乎有些过头,但是如果您正在构建一个较大的应用程序,则可以很快获得回报。


5

测试相似代码的一种常用方法是提取一个包含Scanner和PrintWriter的方法,类似于此StackOverflow答案,然后测试一下:

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

当然,除了创建重载方法之外,还可以将“扫描程序”和“输出”保留为被测系统中的可变字段。我倾向于让课程尽可能保持无状态,但是这对您或您的同事/讲师来说不是一个很大的让步。

您还可以选择将测试代码与被测代码放在同一Java包中(即使它位于不同的源文件夹中),这使您可以放宽两个参数重载的可见性,使其成为包私有的。


您的意思是进行测试,方法processUserInput()调用method(in,out)而不是processUserInput(in,out)吗?
NadZ

@NadZ当然;我从一个通用示例开始,然后将其完全更改为针对该问题。
杰夫·鲍曼

3

我设法找到一种更简单的方法。但是,你必须使用外部库System.rules通过@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;
  }
}

测试

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

但是,在我的情况下,问题是我的第二个输入有空格,这使整个输入流为空!


这很好用...我不知道您所说的“输入流为空”是什么意思。好的是,inputEntry = scanner.nextLine();当与with一起使用时,System.in它将始终等待用户(并且将空行接受为empty String)...而在您使用systemInMock.provideLines()该行提供NoSuchElementException行时,当行用完时将抛出该错误。这确实使您很容易就不会过多地“扭曲”应用程序代码以迎合测试需求。
mike啮齿动物

我不记得是什么问题了,但是您是对的,我再次检查了我的代码,发现我以两种方式对其进行了修复:1.使用systemInMock:systemInMock.provideLines("1", "2"); 2.使用System.setIn而没有外部库:String data2 = "1 2"; System.setIn(new ByteArrayInputStream(data2.getBytes()));
Omar Elshal

2

您可能首先提取从键盘检索数字的逻辑到其自己的方法中。然后,您可以测试验证逻辑,而不必担心键盘。为了测试keyboard.nextInt()调用,您可能需要考虑使用模拟对象。


2
请注意,您不能模拟Scanner对象(它们是最终的)。
hoipolloi 2011年

2

我已经解决了从stdin读取以模拟控制台的问题...

我的问题是我想尝试在JUnit中编写测试控制台以创建特定对象...

问题就像您所说的一样:如何从JUnit测试中用Stdin编写代码?

然后在大学里,我了解到重定向,就像你说的System.setIn(InputStream)更改stdin filedescriptor一样,然后可以编写...

但是还有一个问题可以解决... JUnit测试块等待从新InputStream读取,因此您需要创建一个线程从InputStream读取,并从JUnit测试线程在新的Stdin中写入...首先,您必须之所以用Stdin进行写是因为,如果您稍后写创建要从stdin读取的线程,则您可能会遇到竞争条件...您可以在输入流中进行写入,然后再进行读取...或者可以在输入流中进行读取,然后再进行写入...

这是我的代码,我的英语能力很差,希望您能理解所有问题以及从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();      
}

1

我发现创建一个定义类似于java.io.Console的方法的接口,然后将其用于读取或写入System.out很有帮助。实际的实现将委派给System.console(),而您的JUnit版本可以是具有固定输入和预期响应的模拟对象。

例如,您将构建一个MockConsole,其中包含来自用户的罐头输入。每次调用readLine时,mock实现都会从列表中弹出一个输入字符串。它还将收集所有写入响应列表的输出。在测试结束时,如果一切顺利,则将读取所有输入,并且可以对输出进行断言。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.