Program Club

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

proclub 2020. 11. 1. 19:00
반응형

시뮬레이션 된 사용자 입력을 사용한 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을 자신의 스트림으로 바꿀 수 있습니다 . 입력 스트림은 바이트 배열 일 수 있습니다.

ByteArrayInputStream in = new ByteArrayInputStream("My string".getBytes());
System.setIn(in);

// do your thing

// optionally, reset System.in to its original
System.setIn(System.in)

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

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.


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.


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 have spaces and this makes the whole input stream null !


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

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.

참고URL : https://stackoverflow.com/questions/6415728/junit-testing-with-simulated-user-input

반응형