【问题标题】:JUnit testing with simulated user input使用模拟用户输入进行 JUnit 测试
【发布时间】:2011-09-18 22:23:41
【问题描述】:

我正在尝试为需要用户输入的方法创建一些 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 测试方法中手动执行此操作?喜欢模拟用户输入?

提前致谢。

【问题讨论】:

  • 你到底在测试什么?扫描器?测试方法通常应该断言有用的东西。
  • 您不必测试 Java 的 Scanner 类。您可以手动设置输入并简单地测试您自己的逻辑。 int input = -1 或 5 或 11 将涵盖您的逻辑
  • 六年后……这仍然是一个好问题。尤其是因为在开始开发应用程序时,您通常可能不想拥有 JavaFX 的所有花里胡哨,而是开始使用不起眼的命令行进行一些非常基本的交互。遗憾的是 JUnit 并没有让这变得更容易。对我来说,Omar Elshal 的回答非常好,涉及到最少的“人为”或“扭曲”应用程序编码......

标签: java eclipse junit user-input


【解决方案1】:

您可以替换System.in with you own stream by calling System.setIn(InputStream 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;
}

【讨论】:

  • @KrzyH 我将如何使用多个输入来执行此操作?假设在 //do your thing 我有三个用户输入提示。我该怎么办?
  • @Stupid.Fat.Cat 我添加了第二种解决问题的方法,更加优雅和灵活
  • @KrzyH 对于第二种方法,您将需要在方法定义中将输入流和输出流作为参数传递,这不是我想要的。你知道有什么更好的方法吗?
  • 如何模拟按回车键?现在,程序只是一次性读取所有输入,这不好。我试过\n 但这并没有什么不同
  • @CodyBugstein 使用ByteArrayInputStream in = new ByteArrayInputStream(("1" + System.lineSeparator() + "2").getBytes()); 为不同的keyboard.nextLine() 调用获取多个输入。
【解决方案2】:

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

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

对于你的小例子来说,这似乎有点过头了,但如果你正在构建一个更大的应用程序,这样开发可以很快得到回报。

【讨论】:

    【解决方案3】:

    测试类似代码的一种常见方法是提取一个接受 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.")););
    }
    

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

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

    【讨论】:

    • 你的意思是测试,方法 processUserInput() 调用 method(in, out) 而不是 processUserInput(in, out) ?
    • @NadZ 当然;我从一个一般示例开始,并没有完全将其改回特定于问题的示例。
    【解决方案4】:

    我设法找到了一种更简单的方法。但是,您必须使用 @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;
      }
    }
    

    测试

    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(); 与System.in 一起使用时将始终等待用户(并且将接受空行作为空String)......而当您使用systemInMock.provideLines() 提供行时,这将抛出@987654328 @当行用完时。这真的很容易不要“扭曲”应用程序代码太多以满足测试。
    • 我不记得到底是什么问题,但你是对的,我再次检查了我的代码,发现我通过两种方式修复了它:1. 使用 systemInMock: systemInMock.provideLines("1", "2"); 2. 使用System.setIn 没有外部库:String data2 = "1 2";System.setIn(new ByteArrayInputStream(data2.getBytes()));
    【解决方案5】:

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

    【讨论】:

    • 注意,你不能模拟 Scanner 对象(它们是最终的)。
    【解决方案6】:

    我已经修复了从标准输入读取以模拟控制台的问题...

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

    问题就像你所说的那样:我如何在 JUnit 测试中的 Stdin 中编写代码?

    然后在大学里我学习了重定向,就像你说的 System.setIn(InputStream) 改变了标准输入文件描述符,然后你可以写...

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

    这是我的代码,我的英语水平很差,希望大家能理解问题和解决方案,在 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();      
    }
    

    【讨论】:

      【解决方案7】:

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2023-03-17
        • 1970-01-01
        • 2014-06-14
        • 2019-02-28
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多