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

사용자 입력이 필요한 메서드에 대한 일부 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로 전달할 수있는 방법이 있습니까? 사용자 입력 시뮬레이션처럼?

미리 감사드립니다.



답변

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


답변

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

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

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


답변

유사한 코드를 테스트하는 일반적인 방법 중 하나는 이 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 패키지에 테스트 코드를 넣도록 선택할 수도 있습니다 (다른 소스 폴더에 있더라도). 이렇게하면 두 매개 변수 오버로드의 가시성을 패키지 전용으로 완화 할 수 있습니다.


답변

더 간단한 방법을 찾았습니다. 그러나 @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이됩니다!


답변

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


답변

콘솔을 시뮬레이션하기 위해 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();
}


답변

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

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