JUnit: Как смоделировать тестирование System.in? - PullRequest
48 голосов
/ 30 октября 2009

У меня есть программа командной строки Java. Я хотел бы создать контрольный пример JUnit, чтобы иметь возможность имитировать System.in. Потому что, когда моя программа запускается, она попадает в цикл while и ждет ввода от пользователей. Как мне смоделировать это в JUnit?

Спасибо

Ответы [ 8 ]

60 голосов
/ 30 октября 2009

Технически возможно переключить System.in, но в целом было бы надежнее не вызывать его напрямую в коде, а добавить слой косвенности, чтобы источник входного сигнала контролировался из одной точки вашего приложения. Именно то, как вы это делаете, является деталью реализации - предложения внедрения зависимости хороши, но вам не обязательно вводить сторонние фреймворки; например, вы можете передать контекст ввода-вывода из вызывающего кода.

Как переключить System.in:

String data = "Hello, World!\r\n";
InputStream stdin = System.in;
try {
  System.setIn(new ByteArrayInputStream(data.getBytes()));
  Scanner scanner = new Scanner(System.in);
  System.out.println(scanner.nextLine());
} finally {
  System.setIn(stdin);
}
11 голосов
/ 06 июня 2018

На основании ответа @ McDowell и другого ответа, показывающего, как тестировать System.out , я хотел бы поделиться своим решением, чтобы дать вход для программы и проверить ее вывод .

Для справки я использую JUnit 4.12.

Допустим, у нас есть эта программа, которая просто копирует ввод в вывод:

import java.util.Scanner;

public class SimpleProgram {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.print(scanner.next());
        scanner.close();
    }
}

Чтобы проверить это, мы можем использовать следующий класс:

import static org.junit.Assert.*;

import java.io.*;

import org.junit.*;

public class SimpleProgramTest {
    private final InputStream systemIn = System.in;
    private final PrintStream systemOut = System.out;

    private ByteArrayInputStream testIn;
    private ByteArrayOutputStream testOut;

    @Before
    public void setUpOutput() {
        testOut = new ByteArrayOutputStream();
        System.setOut(new PrintStream(testOut));
    }

    private void provideInput(String data) {
        testIn = new ByteArrayInputStream(data.getBytes());
        System.setIn(testIn);
    }

    private String getOutput() {
        return testOut.toString();
    }

    @After
    public void restoreSystemInputOutput() {
        System.setIn(systemIn);
        System.setOut(systemOut);
    }

    @Test
    public void testCase1() {
        final String testString = "Hello!";
        provideInput(testString);

        SimpleProgram.main(new String[0]);

        assertEquals(testString, getOutput());
    }
}

Я не буду много объяснять, потому что я считаю, что код читабелен, и я привел свои источники.

Когда JUnit запускает testCase1(), он будет вызывать вспомогательные методы в порядке их появления:

  1. setUpOutput(), из-за аннотации @Before 1023 *
  2. provideInput(String data), вызывается с testCase1()
  3. getOutput(), вызывается с testCase1()
  4. restoreSystemInputOutput(), из-за аннотации @After 1035 *

Я не тестировал System.err, потому что мне это не нужно, но это должно быть легко реализовать, подобно тестированию System.out.

8 голосов
/ 30 октября 2009

Есть несколько способов подойти к этому. Наиболее полный способ - передать InputStream при запуске тестируемого класса, который является поддельным InputStream, который передает смоделированные данные вашему классу. Вы можете взглянуть на структуру внедрения зависимостей (например, Google Guice), если вам нужно много делать в своем коде, но простой способ:

 public class MyClass {
     private InputStream systemIn;

     public MyClass() {
         this(System.in);
     }

     public MyClass(InputStream in) {
         systemIn = in;
     }
 }

При тестировании вы вызываете конструктор, который принимает входной поток. Вы даже делаете этот пакет конструктора закрытым и помещаете тест в тот же пакет, чтобы другой код обычно не рассматривал возможность его использования.

5 голосов
/ 30 октября 2009

Попробуйте изменить код, чтобы использовать внедрение зависимостей . Вместо того чтобы использовать метод, который напрямую использует System.in, пусть метод принимает InputStream в качестве аргумента. Затем в своем тесте junit вы сможете пройти тестовую реализацию InputStream вместо System.in.

4 голосов
/ 07 декабря 2015

Вы можете написать четкий тест для интерфейса командной строки, используя правило TextFromStandardInputStream библиотеки Системные правила .

public void MyTest {
  @Rule
  public final TextFromStandardInputStream systemInMock
    = emptyStandardInputStream();

  @Test
  public void readTextFromStandardInputStream() {
    systemInMock.provideLines("foo");
    Scanner scanner = new Scanner(System.in);
    assertEquals("foo", scanner.nextLine());
  }
}

Полное раскрытие: я автор этой библиотеки.

1 голос
/ 22 января 2017

Проблема с BufferedReader.readLine() заключается в том, что это метод блокировки, который ожидает ввода данных пользователем. Мне кажется, что вы не особенно хотите имитировать это (т.е. вы хотите, чтобы тесты были быстрыми). Но в контексте тестирования он постоянно возвращает null на высокой скорости во время тестирования, что утомительно.

Для пуриста вы можете сделать пакет getInputLine ниже приватным и посмеяться над ним: easy-peezy.

String getInputLine() throws Exception {
    return br.readLine();
}

... вам нужно убедиться, что у вас есть способ остановить (обычно) цикл взаимодействия пользователя с приложением. Вам также придется справиться с тем фактом, что ваши «строки ввода» всегда будут одинаковыми, пока вы каким-то образом не измените doReturn вашего макета: вряд ли это типично для пользовательского ввода.

Для не пуристов, которые хотят облегчить себе жизнь (и создать удобочитаемые тесты), вы можете поместить все эти вещи ниже в код своего приложения:

private Deque<String> inputLinesDeque;

void setInputLines(List<String> inputLines) {
    inputLinesDeque = new ArrayDeque<String>(inputLines);
}

private String getInputLine() throws Exception {
    if (inputLinesDeque == null) {
        // ... i.e. normal case, during app run: this is then a blocking method
        return br.readLine();
    }
    String nextLine = null;
    try {
        nextLine = inputLinesDeque.pop();
    } catch (NoSuchElementException e) {
        // when the Deque runs dry the line returned is a "poison pill", 
        // signalling to the caller method that the input is finished
        return "q";
    }

    return nextLine;
}

... в вашем тесте вы можете пойти так:

consoleHandler.setInputLines( Arrays.asList( new String[]{ "first input line", "second input line" }));

перед запуском метода в этом классе "ConsoleHandler", которому требуются строки ввода.

1 голос
/ 30 октября 2009

Вы можете создать пользовательский InputStream и прикрепить его к System классу

class FakeInputStream extends InputStream {

    public int read() {
         return -1;
    }
}

А затем используйте его с Scanner

System.in = new FakeInputStream();

До:

InputStream in = System.in;
...
Scanner scanner = new Scanner( in );

После того, как:

InputStream in = new FakeInputStream();
...
Scanner scanner = new Scanner( in );

Хотя я думаю, что вам лучше проверить, как ваш класс должен работать с данными, считанными из входного потока, а не с тем, как он читает оттуда.

0 голосов
/ 21 мая 2017

может быть так (не проверено):

InputStream save_in=System.in;final PipedOutputStream in = new PipedOutputStream(); System.setIn(new PipedInputStream(in));

in.write("text".getBytes("utf-8"));

System.setIn( save_in );

больше деталей:

//PrintStream save_out=System.out;final ByteArrayOutputStream out = new ByteArrayOutputStream();System.setOut(new PrintStream(out));

InputStream save_in=System.in;final PipedOutputStream in = new PipedOutputStream(); System.setIn(new PipedInputStream(in));

//start something that reads stdin probably in a new thread
//  Thread thread=new Thread(new Runnable() {
//      @Override
//      public void run() {
//          CoursesApiApp.main(new String[]{});                 
//      }
//  });
//  thread.start();


//maybe wait or read the output
//  for(int limit=0; limit<60 && not_ready ; limit++)
//  {
//      try {
//          Thread.sleep(100);
//      } catch (InterruptedException e) {
//          e.printStackTrace();
//      }
//  }


in.write("text".getBytes("utf-8"));

System.setIn( save_in );

//System.setOut(save_out);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...