シミュレートされたユーザー入力を使用した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 input = -1または5または11がロジックをカバーします
blong824 2011年

3
6年後...そしてそれはまだ良い質問です。特に、アプリの開発を開始するときは、通常、JavaFXのすべての機能を使用したくない場合がありますが、その代わりに、非常に基本的な操作のために控えめなコマンドラインを使用し始めます。JUnitがこれをそれほど簡単にしないのは残念です。私にとって、Omar 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複数の入力でこれを行うにはどうすればよいですか?//あなたのことをするのに、ユーザー入力のための3つのプロンプトがあるとしましょう。どうすればいいですか?
Stupid.Fat.Cat 2014年

1
@ Stupid.Fat.Cat問題への2番目のアプローチを追加しました。よりエレガントでわかりやすい
KrzyH

1
@KrzyH 2番目のアプローチでは、入力ストリームと出力ストリームをメソッド定義のパラメーターとして渡す必要がありますが、これは私が探しているものではありません。もっと良い方法を知っていますか?
chirag 2015年

6
エンターキーを押すことをシミュレートするにはどうすればよいですか?今のところ、プログラムはすべての入力を1回のショットで読み取るだけですが、これは良くありません。試しました\nが、違いはありません
CodyBugstein 2015年

3
@CodyBugsteinByteArrayInputStream in = new ByteArrayInputStream(("1" + System.lineSeparator() + "2").getBytes());さまざまなkeyboard.nextLine()呼び出しに対して複数の入力を取得するために使用します。
粘土の瓶

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

同様のコードをテストする一般的な方法の1つは、このStackOverflowの回答と同様に、スキャナーと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パッケージに配置することもできます(別のソースフォルダーにある場合でも)。これにより、2つのパラメーターのオーバーロードの可視性を緩和してパッケージプライベートにすることができます。


テストするつもりでしたが、メソッドprocessUserInput()は、processUserInput(in、out)ではなくmethod(in、out)を呼び出しますか?
nadZ 2016

@NadZもちろん; 私は一般的な例から始めて、それを質問に固有のものに不完全に戻しました。
ジェフボウマン2016

4

私はなんとかもっと簡単な方法を見つけることができました。ただし、@ StefanBirknerによる外部ライブラリ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());
  }
}

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


これは非常にうまく機能します...「入力ストリームnull」の意味がわかりません。良い点は、でinputEntry = scanner.nextLine();使用するSystem.inと常にユーザーを待機する(そして空の行を空として受け入れるString)...一方、systemInMock.provideLines()これを使用して行を指定するとNoSuchElementException、行がなくなるとスローされます。これにより、テストに対応するためにアプリコードを「歪め」すぎないようにすることが非常に簡単になります。
マイクげっ歯類

何が問題だったのか正確には覚えていませんが、正しいです。コードをもう一度確認したところ、2つの方法で修正したことに気付きました。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年

2

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

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

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

それから大学で、System.setIn(InputStream)がstdinファイル記述子を変更すると言うようなリダイレクトについて学び、それから書き込むことができます...

しかし、修正する問題がもう1つあります...新しいInputStreamからの読み取りを待機しているJUnitテストブロックなので、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();      
}

1

java.io.Consoleに似たメソッドを定義するインターフェースを作成し、それを使用してSystem.outの読み取りまたは書き込みを行うと便利であることがわかりました。実際の実装はSystem.console()に委任されますが、JUnitバージョンは、定型入力と期待される応答を備えたモックオブジェクトにすることができます。

たとえば、ユーザーからの定型入力を含むMockConsoleを作成します。モック実装は、readLineが呼び出されるたびに、入力文字列をリストからポップします。また、応答のリストに書き込まれたすべての出力を収集します。テストの最後に、すべてがうまくいけば、すべての入力が読み取られ、出力でアサートできます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.