【问题标题】:junit testing for user input using Scanner使用 Scanner 对用户输入进行 junit 测试
【发布时间】:2023-03-17 01:04:01
【问题描述】:

我必须在一个使用 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)但不知道怎么做。

【问题讨论】:

标签: java junit mocking mockito


【解决方案1】:

您可以使用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,它从console 读取(因此您在控制台中输入)。但是您只是对其进行了修改,并让系统改为从提供的inputstream 中读取。所以它不再从控制台读取,而是从提供的输入流中读取。

【讨论】:

  • 它工作了:)。你能告诉我这是如何工作的吗?
  • 优秀的解决方案
【解决方案2】:

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

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
  • @Stefan 您的解决方案运行良好,添加此依赖项,一切顺利! <dependency>`com.github.stefanbirkner`<artifactId>system-rules</artifactId><version>1.16.0</version></dependency>
【解决方案3】:

除了 switching System.in,正如 Codebender 也提到的,考虑重构,以便 getInput() 成为对您编写的彻底 getInput(Scanner) 方法的单行调用,您可以通过创建自己的 Scanner("your\ntest\ninput\n") 轻松测试该方法。还有许多其他方法可以注入您的扫描仪依赖项,例如为测试创建一个覆盖字段,但是仅使方法重载非常容易,并且在技术上为您提供了更大的灵活性(让您添加一个功能以从文件中读取输入,例如)。

一般来说,请记住进行易于测试的设计,并且对高风险部分进行比低风险部分更重的测试。这意味着重构是一个很好的工具,而且测试getInput(Scanner) 可能比测试getInput() 重要得多,尤其是当你做的不仅仅是调用nextLine()

我强烈建议不要创建一个模拟 Scanner:模拟一个你不拥有的类型不仅是不好的做法,而且 Scanner 代表了一个非常大的相互关联的方法 API,其中调用顺序很重要。在 Mockito 中复制它意味着您要么在 Mockito 中创建一个大的假 Scanner 实现,要么模拟一个仅测试您所做的调用的最小实现(如果您的实现发生更改,即使您的更改提供了正确的结果也会中断)。使用真正的 Scanner 并将 Mockito 练习保存为外部服务调用或模拟您定义的小型但未编写的 API 的情况。

【讨论】:

    【解决方案4】:

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

    您模拟不起作用的原因是因为您每次都在 getInput() 方法中创建 actual 扫描仪对象。因此,无论你做什么,你的 mockito 实例都不会被调用。因此,使此类可测试的正确方法是识别该类的所有外部依赖项(在本例中为 java.util.Scanner 并通过构造函数将它们注入到类中。这样您就可以在测试期间注入模拟 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。因为我在这个类中没有更多可变状态,所以这个类是线程安全的。

    基于构造函数的依赖注入的概念非常酷,值得在互联网上阅读。它有助于开发良好的线程安全可测试代码。

    【讨论】:

    • 无法模拟扫描仪,因为该类是最终的。
    猜你喜欢
    • 2011-09-18
    • 1970-01-01
    • 1970-01-01
    • 2014-06-14
    • 1970-01-01
    • 2015-08-08
    • 2023-03-29
    • 1970-01-01
    相关资源
    最近更新 更多