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

发布于 2024-11-16 13:52:48 字数 489 浏览 5 评论 0原文

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

I am trying to create some JUnit tests for a method that requires user input. The method under test looks somewhat like the following method:

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

Is there a possible way to automatically pass the program an int instead of me or someone else doing this manually in the JUnit test method? Like simulating the user input?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

我做我的改变 2024-11-23 13:52:48

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

You can replace System.in with you own stream by calling System.setIn(InputStream in).
InputStream can be a byte array:

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

Different approach can be make this method more testable by passing IN and OUT as parameters:

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;
}
蓝天白云 2024-11-23 13:52:48

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

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

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

To test drive your code, you should create a wrapper for system input/output functions. You can do this using dependency injection, giving us a class that can ask for new integers:

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

Then you can create tests for your function, using a mock framework (I use 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.");
}

Then write your function that passes the tests. The function is much cleaner since you can remove the asking/getting integer duplication and the actual system calls are encapsulated.

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

This may seem like overkill for your small example, but if you are building a larger application developing like this can payoff rather quickly.

最丧也最甜 2024-11-23 13:52:48

测试类似代码的一种常见方法是提取一个接受 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 包中(即使它位于不同的源文件夹中),这允许您将两个参数重载的可见性放宽为包私有。

One common way to test similar code would be to extract a method that takes in a Scanner and a PrintWriter, similar to this StackOverflow answer, and test that:

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

Do note that you won't be able to read your output until the end, and you'll have to specify all of your input up front:

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

Of course, rather than creating an overload method, you could also keep the "scanner" and "output" as mutable fields in your system under test. I tend to like keeping classes as stateless as possible, but that's not a very big concession if it matters to you or your coworkers/instructor.

You might also choose to put your test code in the same Java package as the code under test (even if it's in a different source folder), which allows you to relax the visibility of the two parameter overload to be package-private.

木落 2024-11-23 13:52:48

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

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

I managed to find a simpler way. However, you have to use external library System.rules by @Stefan Birkner

I just took the example provided there, I think it couldn't have gotten more simpler:

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

The problem however in my case my second input has spaces and this makes the whole input stream null!

深海里的那抹蓝 2024-11-23 13:52:48

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

You might start by extracting out the logic that retrieves the number from the keyboard into its own method. Then you can test the validation logic without worrying about the keyboard. In order to test the keyboard.nextInt() call you may want to consider using a mock object.

痴意少年 2024-11-23 13:52:48

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

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

问题就像您所说的那样:我怎样才能写在 JUnit 测试的 Stdin 中?

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

但是还有一个问题需要修复...JUnit 测试块等待从新的 InputStream 读取,所以你需要创建一个线程来从InputStream读取并从JUnit测试线程写入新的Stdin...首先你必须在Stdin中写入,因为如果你稍后编写创建线程以从stdin读取你可能会有比赛条件...你可以在读取之前先写入InputStream,或者您可以在写入之前从InputStream读取...

这是我的代码,我的英语水平很差我希望大家都能理解问题以及从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();      
}

I have fixed the problem about read from stdin to simulate a console...

My problems was I'd like try write in JUnit test the console to create a certain object...

The problem is like all you say : How Can I write in the Stdin from JUnit test?

Then at college I learn about redirections like you say System.setIn(InputStream) change the stdin filedescriptor and you can write in then...

But there is one more proble to fix... the JUnit test block waiting read from your new InputStream, so you need create a thread to read from the InputStream and from JUnit test Thread write in the new Stdin... First you have to write in the Stdin because if you write later of create the Thread to read from stdin you likely will have race Conditions... you can write in the InputStream before to read or you can read from InputStream before write...

This is my code, my english skill is bad I hope all you can understand the problem and the solution to simulate write in stdin from JUnit test.

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();      
}
千柳 2024-11-23 13:52:48

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

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

I've found it helpful to create an interface that defines methods similar to java.io.Console and then use that for reading or writing to the System.out. The real implementation will delegate to System.console() while your JUnit version can be a mock object with canned input and expected responses.

For example, you'd construct a MockConsole that contained the canned input from the user. The mock implementation would pop an input string off the list each time readLine was called. It would also gather all of the output written to a list of responses. At the end of the test, if all went well, then all of your input would have been read and you can assert on the output.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文