JUnit: Как имитировать тестирование System.in?
У меня есть программа командной строки Java. Я хотел бы создать тестовый пример JUnit, чтобы смоделировать System.in
. Потому что, когда моя программа запускается, она попадает в цикл while и ждет ввода от пользователей. Как имитировать это в JUnit?
Спасибо
Ответы
Ответ 1
Технически возможно переключить 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);
}
Ответ 2
Есть несколько способов приблизиться к этому. Наиболее полным способом является переход в InputStream при запуске тестируемого класса, который является поддельным InputStream, который передает смоделированные данные в ваш класс. Вы можете взглянуть на инфраструктуру инъекций зависимостей (например, Google Guice), если вам нужно сделать это очень много в своем коде, но простой способ:
public class MyClass {
private InputStream systemIn;
public MyClass() {
this(System.in);
}
public MyClass(InputStream in) {
systemIn = in;
}
}
В рамках теста вы вызываете конструктор, который принимает входной поток. Вы облака даже сделаете этот пакет конструктора приватным и поместите тест в тот же пакет, чтобы другой код вообще не рассматривал его использование.
Ответ 3
Попробуйте реорганизовать свой код, чтобы использовать инъекцию зависимостей. Вместо того, чтобы иметь метод, который использует System.in
напрямую, попросите метод принять InputStream
в качестве аргумента. Затем в вашем тесте junit вы сможете передать тестовую реализацию InputStream
вместо System.in
.
Ответ 4
Вы можете написать четкий тест для интерфейса командной строки, используя правило 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());
}
}
Полное раскрытие: я являюсь автором этой библиотеки.
Ответ 5
Основываясь на ответе @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()
, он будет вызывать вспомогательные методы в следующем порядке:
-
setUpOutput()
, из-за аннотации @Before
-
provideInput(String data)
, вызываемые из testCase1()
-
getOutput()
, вызываемый из testCase1()
-
restoreSystemInputOutput()
, из-за @After
аннотации
Я не тестировал System.err
потому что мне это не нужно, но его должно быть легко реализовать, как и при тестировании System.out
.
Ответ 6
Вы можете создать пользовательский InputStream
и прикрепить его к классу System
class FakeInputStream extends InputStream {
public int read() {
return -1;
}
}
И затем используйте его со своим Scanner
System.in = new FakeInputStream();
удаp >
До:
InputStream in = System.in;
...
Scanner scanner = new Scanner( in );
После:
InputStream in = new FakeInputStream();
...
Scanner scanner = new Scanner( in );
Хотя я думаю, вам лучше проверить, как ваш класс должен работать с данными, считываемыми из потока ввода, и не так, как он читает оттуда.
Ответ 7
Проблема с BufferedReader.readLine()
заключается в том, что это метод блокировки, который ждет ввода пользователя. Мне кажется, что вы не особенно хотите имитировать это (т.е. Вы хотите, чтобы тесты были быстрыми). Но в тестовом контексте он постоянно возвращает null
на высокой скорости во время тестирования, что очень неприятно.
Для пуриста вы можете сделать getInputLine
ниже package-private и издеваться над ним: легко 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, который нуждается в строках ввода.
Ответ 8
может быть так (не проверено):
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);