web-dev-qa-db-ja.com

シミュレートされたユーザー入力を使用した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を渡す方法はありますか?ユーザー入力のシミュレーションが好きですか?

前もって感謝します。

70
Wimpey

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

コードをテストドライブするには、システムの入出力関数のラッパーを作成する必要があります。依存性注入を使用してこれを行うことができ、新しい整数を要求できるクラスを提供します。

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

これはあなたの小さな例にとってはやり過ぎのように思えるかもしれませんが、このように開発するより大きなアプリケーションを構築している場合は、かなり迅速に成果を上げることができます。

16
Garrett Hall

同様のコードをテストする一般的な方法の1つは、スキャナーとPrintWriterを取り込むメソッドを抽出することです。これは this StackOverflow answer に似ており、以下をテストします。

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パッケージに(別のソースフォルダーにある場合でも))テストコードを配置することもできます。これにより、2つのパラメーターの可視性を緩和できます。オーバーロードはパッケージプライベートになります。

5
Jeff Bowman

もっと簡単な方法を見つけることができました。ただし、外部ライブラリを使用する必要があります System.rules By @Stefan Birkner

そこで提供された例を取り上げただけで、もっと簡単になったはずがないと思います:)

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

ただし、私の場合、2番目の入力にスペースがあり、これにより入力ストリーム全体がヌルになります。

2
Omar Elshal

まず、キーボードから番号を取得するロジックを独自のメソッドに抽出することから始めます。その後、キーボードを気にせずに検証ロジックをテストできます。 keyboard.nextInt()呼び出しをテストするために、モックオブジェクトの使用を検討することができます。

2
Sarah Haskins

コンソールをシミュレートするための標準入力からの読み取りに関する問題を修正しました...

私の問題は、特定のオブジェクトを作成するためにコンソールをテストするJUnitで書きたいと思っていました...

問題はあなたが言うすべてのようです:どのようにJUnitテストからStdinに書くことができますか?

それから大学では、System.setIn(InputStream)がstdin filedescriptorを変更すると言うようにリダイレクトについて学びます。

しかし、もう1つ修正すべき問題があります。JUnitテストブロックが新しいInputStreamからの読み取りを待機しているため、InputStreamおよびJUnitテストから読み取るスレッドを作成する必要があります。後で標準入力から読み込むスレッドを作成する場合、競合条件が発生する可能性があるため、標準入力に書き込みます。入力前に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が呼び出されるたびにリストから入力文字列をポップします。また、応答のリストに書き込まれたすべての出力を収集します。テストの終了時に、すべてがうまくいけば、すべての入力が読み取られ、出力でアサートできます。

1
massfords