使用模拟用户输入进行JUnit测试

时间:2011-06-20 18:28:21

标签: java eclipse junit user-input

我正在尝试为需要用户输入的方法创建一些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而不是我或其他人手动执行此操作?像模拟用户输入一样?

提前致谢。

7 个答案:

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