使用Scanner进行用户输入的Junit测试

23

我需要测试一个类中使用Scanner类接收输入的方法。

package com.math.calculator;

import java.util.Scanner;

public class InputOutput {

    public String getInput() {
        Scanner sc = new Scanner(System.in);
        return sc.nextLine();
    }
}

我想使用JUnit进行测试,但不确定如何操作。

我尝试过使用以下代码,但它无法正常工作。

package com.math.calculator;

import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class InputOutputTest {

    @Test
    public void shouldTakeUserInput() {
        InputOutput inputOutput= new InputOutput();

        assertEquals("add 5", inputOutput.getInput());
    }
}

我也想用Mockito(使用mock... when ... thenReturn)来尝试一下,但不确定该如何做。


4个回答

37

您可以使用System.setIn()方法更改System.in流。

尝试一下,

@Test
public void shouldTakeUserInput() {
    InputOutput inputOutput= new InputOutput();

    String input = "add 5";
    InputStream in = new ByteArrayInputStream(input.getBytes());
    System.setIn(in);

    assertEquals("add 5", inputOutput.getInput());
}

您刚刚修改了System.in字段。System.in基本上是一个从控制台读取的InputStream(因此您在控制台输入)。但是,您刚刚修改它,并让系统从提供的inputstream读取。因此,它将不再从控制台读取,而是从提供的输入流中读取。


2
它成功了:)。你能告诉我这是如何工作的吗? - Ross
优秀的解决方案 - Melad Basilius

9

您可以使用System Rules库的规则来为命令行界面编写清晰的测试。

public void MyTest {
  @Rule
  public final TextFromStandardInputStream systemInMock
    = emptyStandardInputStream();

  @Test
  public void shouldTakeUserInput() {
    systemInMock.provideLines("add 5", "another line");
    InputOutput inputOutput = new InputOutput();
    assertEquals("add 5", inputOutput.getInput());
  }
}

目前为止最好的方法,尽管它需要 system-rules - Alex
2
@Stefan,你的解决方案非常好,只需添加此依赖项即可! com.github.stefanbirkner system-rules 1.16.0 - daemonThread

3
除了切换 System.in之外,正如Codebender所提到的那样,考虑重构使得getInput()成为一个对你自己编写的全面getInput(Scanner)方法的一行调用,你可以通过创建自己的Scanner("your\ntest\ninput\n")轻松测试。还有许多其他注入扫描仪依赖项的方法,例如制作一个你可以为测试覆盖的字段,但只是制作一个方法重载非常容易,并且在技术上给你更多的灵活性(例如允许你添加从文件中读取输入的功能)。
总的来说,请记住设计易于测试,并对高风险部分进行更重的测试而不是低风险部分。这意味着重构是一个好工具,并且测试getInput(Scanner)可能比测试getInput()更重要,特别是当你做的不仅仅是调用nextLine()时。
我强烈建议不要创建一个模拟Scanner:不仅是模拟你不拥有的类型是不好的实践,而且Scanner表示一个非常庞大的API,其中调用顺序很重要。在Mockito中复制它意味着你要么创建一个大型的假Scanner实现,要么模拟一个最小的实现,只测试你所做的调用(如果你的实现发生变化,即使你的更改提供了正确的结果,也会出现问题)。使用真正的Scanner,并将Mockito实践保留给外部服务调用或模拟您定义的尚未编写的小型API的情况。

-2

首先,我假设您的测试目标是验证用户输入是否从扫描仪获取,并且返回的值是已在扫描仪中输入的值。

模拟不起作用的原因是因为您每次都在getInput()方法中创建实际扫描仪对象。因此,无论您做什么,mockito实例都不会被调用。因此,使这个类可测试的正确方法是通过构造函数识别类的所有外部依赖项(在本例中为java.util.Scanner),并将它们注入到类中。这样,在测试期间可以注入模拟扫描仪实例。这是依赖注入的基本步骤,进而导致良好的TDD。以下是一个示例:

 package com.math.calculator;

    import java.util.Scanner;

    public class InputOutput {

        private final Scanner scanner;

        public InputOutput()
        {
           //the external exposed default constructor 
           //would use constructor-chaining to pass an instance of Scanner.

           this(new Scanner(System.in));
        }

        //declare a package level constructor that would be visible only to the test class. 
      //It is a good practice to have a class and it's test within the same     package.
        InputOutput(Scanner scanner)
        {
            this.scanner  = scanner;
        }

        public String getInput() {

            return scanner.nextLine();
        }
    }

现在是你的测试方法:

@Test
public void shouldTakeUserInput() {
    //create a mock scanner
    Scanner mockScanner = mock(Scanner.class);
    //set up the scanner
    when(mockScanner.nextLine()).thenReturn("add 5");

    InputOutput inputOutput= new InputOutput(mockScanner);

    //assert output
    assertEquals("add 5", inputOutput.getInput());

   //added bonus - you can verify that your scanner's nextline() method is
   //actually called See Mockito.verify
   verify(mockScanner).nextLine();
}

另外要注意的是,由於在上述類別中我是使用構造函數進行注入,所以我必須聲明 Scanner 實例為 final。由於在此類別中已經沒有可變狀態,所以此類別是線程安全的。
基於構造函數的依賴注入的概念非常酷,值得在互聯網上閱讀相關文章。它對於開發良好的線程安全測試代碼有很大幫助。

6
无法对Scanner进行模拟,因为该类是final的。 - Migs

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接