시뮬레이션 된 사용자 입력을 사용한 JUnit 테스트


82

사용자 입력이 필요한 메서드에 대한 일부 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
정확히 무엇을 테스트하고 있습니까? 스캐너? 테스트 방법은 일반적으로 유용하다고 주장해야합니다.
Markus

2
Java의 Scanner 클래스를 테스트 할 필요가 없습니다. 수동으로 입력을 설정하고 단순히 자신의 논리를 테스트 할 수 있습니다. int 입력 = -1 또는 5 또는 11이 논리를 다룹니다
blong824 2011-06-20

3
6 년이 지난 지금도 좋은 질문입니다. 특히 앱 개발을 시작할 때 일반적으로 JavaFX의 모든 종소리와 휘파람을 원하지 않을 수 있지만 대신 매우 기본적인 상호 작용을 위해 겸손한 명령 줄을 사용하기 시작하면됩니다. JUnit이 이것을 아주 쉽게 만들지 못한다는 점이 부끄러운 일입니다. 나를 위해 오마르 Elshal의 대답은 "고안"최소 또는 "왜곡"응용 프로그램이 포함 된 코딩으로, 아주 좋은 ...
마이크 설치류

답변:


105

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

6
@KrzyH 여러 입력으로 어떻게할까요? // do your thing에서 사용자 입력에 대한 세 가지 프롬프트가 있다고 말합니다. 어떻게해야하나요?
Stupid.Fat.Cat 2014 년

1
@ Stupid.Fat.Cat 나는 문제에 대한 두 번째 접근 방식을 추가했습니다. 더 우아하고 쾌적합니다
KrzyH

1
@KrzyH 두 번째 접근 방식에서는 입력 스트림과 출력 스트림을 메서드 정의의 매개 변수로 전달해야합니다. 더 나은 방법을 알고 있습니까?
Chirag

6
Enter 키 누르기를 어떻게 시뮬레이션합니까? 지금은 프로그램이 한 번에 모든 입력을 읽습니다. 이는 좋지 않습니다. 시도 \n했지만 차이가 없습니다
CodyBugstein

3
@CodyBugstein ByteArrayInputStream in = new ByteArrayInputStream(("1" + System.lineSeparator() + "2").getBytes());서로 다른 keyboard.nextLine()호출에 대해 여러 입력을 가져 오는 데 사용 합니다.
A Jar of Clay

18

코드를 테스트하려면 시스템 입력 / 출력 함수에 대한 래퍼를 만들어야합니다. 종속성 주입을 사용하여이를 수행 할 수 있으며 새 정수를 요청할 수있는 클래스를 제공합니다.

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

이것은 당신의 작은 예제에 대해 과잉처럼 보일 수 있지만, 이와 같이 개발하는 더 큰 애플리케이션을 구축하는 경우 다소 빨리 결과를 얻을 수 있습니다.


6

유사한 코드를 테스트하는 일반적인 방법 중 하나는 이 StackOverflow 답변 과 유사한 Scanner 및 PrintWriter를받는 메서드를 추출 하고 다음을 테스트하는 것입니다.

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 () 메서드는 processUserInput (in, out)이 아닌 method (in, out)를 호출합니까?
NadZ

@NadZ 물론입니다. 나는 일반적인 예로 시작하여 질문에 대한 구체적인 것으로 불완전하게 변경했습니다.
Jeff Bowman

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

그러나 문제는 내 경우 두 번째 입력에 공백이 있으며 전체 입력 스트림이 null이됩니다!


이것은 매우 잘 작동합니다. "입력 스트림 null"이 무엇을 의미하는지 모르겠습니다. 좋은 점은 inputEntry = scanner.nextLine();with System.in를 사용 하면 항상 사용자를 기다릴 것이고 (빈 줄을 빈 줄로 받아 들일 것입니다 String) ... 반면에 systemInMock.provideLines()이것을 사용하여 줄을 제공 NoSuchElementException하면 줄이 다 떨어질 때 던질 것입니다. 이렇게하면 테스트를 위해 앱 코드를 너무 많이 "왜곡"하지 않기가 쉽습니다.
마이크 설치류

문제가 무엇인지 정확히 기억하지 못합니다.하지만 귀하가 맞습니다. 코드를 다시 확인한 결과 다음 두 가지 방법으로 문제가 해결되었음을 알게되었습니다. 1. systemInMock 사용 : systemInMock.provideLines("1", "2"); 2. 외부 라이브러리없이 System.setIn 사용 :String data2 = "1 2"; System.setIn(new ByteArrayInputStream(data2.getBytes()));
Omar Elshal

2

키보드에서 숫자를 검색하는 논리를 자체 메서드로 추출하여 시작할 수 있습니다. 그런 다음 키보드에 대한 걱정없이 유효성 검사 논리를 테스트 할 수 있습니다. keyboard.nextInt () 호출을 테스트하기 위해 모의 객체 사용을 고려할 수 있습니다.


2
Scanner 개체를 모의 처리 할 수 ​​없습니다 (최종 개체 임).
hoipolloi 2011-06-20

2

콘솔을 시뮬레이션하기 위해 stdin에서 읽기에 대한 문제를 수정했습니다.

내 문제는 특정 개체를 생성하기 위해 콘솔을 JUnit 테스트에서 작성하고 싶습니다 ...

문제는 당신이 말하는 모든 것과 같습니다. JUnit 테스트에서 Stdin으로 어떻게 쓸 수 있습니까?

그런 다음 대학에서 System.setIn (InputStream) stdin filedescriptor를 변경하고 다음과 같이 쓸 수 있다고 말하는 것과 같은 리디렉션에 대해 배웁니다.

하지만 수정해야 할 문제가 하나 더 있습니다 ... JUnit 테스트 블록이 새 InputStream에서 읽기를 기다리고 있으므로 InputStream에서 읽을 스레드를 만들고 새 Stdin에서 JUnit 테스트 스레드를 작성해야합니다. 먼저해야 할 일 나중에 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();      
}

1

java.io.Console과 유사한 메서드를 정의하는 인터페이스를 만든 다음이를 System.out에 읽거나 쓰는 데 사용하는 것이 유용하다는 것을 알았습니다. 실제 구현은 System.console ()에 위임되지만 JUnit 버전은 미리 준비된 입력 및 예상 응답이있는 모의 객체 일 수 있습니다.

예를 들어, 사용자의 미리 준비된 입력을 포함하는 MockConsole을 생성합니다. 모의 구현은 readLine이 호출 될 때마다 목록에서 입력 문자열을 팝합니다. 또한 응답 목록에 기록 된 모든 출력을 수집합니다. 테스트가 끝날 때 모든 것이 잘 되었다면 모든 입력을 읽었을 것이며 출력에 대해 주장 할 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.