JUnit: Как смоделировать тестирование System.in?

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

Спасибо

9 ответов

Решение

Технически возможно переключить 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);
}

Основываясь на ответе @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 аннотирование
  2. provideInput(String data)позвонил из testCase1()
  3. getOutput()позвонил из testCase1()
  4. restoreSystemInputOutput()из-за @After аннотирование

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

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

 public class MyClass {
     private InputStream systemIn;

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

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

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

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

Вы можете написать четкий тест для интерфейса командной строки, используя 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());
  }
}

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

Вы можете создать кастом 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 );

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

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

Для пуриста вы можете сделать getInputLine ниже пакет-приват, и издевайтесь над ним: легко-пише.

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", который нуждается в строках ввода.

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

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

@Стефан Биркнер, спасибо!

  1. Изменить Pom.xml
      Ref:
https://stackoverflow.com/a/66127606/8317677
https://github.com/stefanbirkner/system-lambda/blob/master/pom.xml
https://github.com/stefanbirkner/system-lambda/blob/master/src/test/java/com/github/stefanbirkner/systemlambda/WithTextFromSystemInTest.java

  <properties>
      <system-lambda.version>1.2.1</system-lambda.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>com.github.stefanbirkner</groupId>
      <artifactId>system-lambda</artifactId>
      <version>${system-lambda.version}</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
  1. Добавить код функции
      import java.io.BufferedReader;
import java.io.InputStreamReader;

public class SimpleProgram003 {
    public static void main(String[] args) {
        try{
            String c;
            BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
            do{
                c = in.readLine();
                System.out.println(c);
                String d = c;
            }while(!c.equals("q"));
        }catch(Exception e){
            System.out.println("catch Exception");
        }
    }
}
  1. Добавить тестовый код
      import static com.github.stefanbirkner.systemlambda.SystemLambda.*;
import static org.junit.Assert.*;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.PrintStream;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

/**
 * Unit test for simple App. JUnit 4.x.
 */
public class SimpleProgram003Test {
    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 setInput(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 1\nq\n";
        setInput(testString);

        SimpleProgram003.main(new String[0]);
        // String a = getOutput();
        assertEquals("Hello 1\r\nq\r\n", getOutput());
    }

    @Test // Multiply inputs
    public void testCase2() throws Exception {
        withTextFromSystemIn(
            "Input1",
            "Input2",
            "q",
            "Input3"
        ).execute(() -> {
            SimpleProgram003.main(new String[0]);
            // String a = getOutput();
            assertEquals("Input1\r\nInput2\r\nq\r\n", getOutput());
        });
    }
}
Другие вопросы по тегам