Подтвердить что ты не робот

Тестирование 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;
}

Есть ли способ автоматически передать программу int вместо меня или кого-то другого, делающего это вручную в методе тестирования JUnit? Как моделирование ввода пользователя?

Спасибо заранее.

4b9b3361

Ответ 1

Вы можете заменить System.in своим собственным потоком, вызвав System.setIn(InputStream 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;
}

Ответ 2

Чтобы протестировать ваш код, вы должны создать оболочку для системных функций ввода/вывода. Вы можете сделать это с помощью инъекции зависимостей, предоставив нам класс, который может запрашивать новые целые числа:

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

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

Ответ 3

Одним из распространенных способов тестирования аналогичного кода было бы извлечение метода, который использует сканер и PrintWriter, аналогичный этому ответу StackOverflow, и проверьте, что:

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, что и тестируемый код (даже если он находится в другой исходной папке), что позволяет вам ослабить видимость двух перегрузок параметров, которые должны быть приватными к пакету.

Ответ 4

Вы можете начать с извлечения логики, которая возвращает номер с клавиатуры в свой собственный метод. Затем вы можете проверить логику проверки, не беспокоясь о клавиатуре. Чтобы протестировать вызов keyboard.nextInt(), вы можете рассмотреть использование макетного объекта.

Ответ 5

Я нашел полезным создать интерфейс, который определяет методы, похожие на java.io.Console, а затем использовать его для чтения или записи в System.out. Реальная реализация будет делегировать System.console(), в то время как ваша версия JUnit может быть макетным объектом с законченными вводами и ожидаемыми ответами.

Например, вы должны создать MockConsole, содержащую консервированный вход пользователя. Макетная реализация будет вызывать входную строку из списка каждый раз, когда вызывается readLine. Он также будет собирать весь вывод, записанный в список ответов. В конце теста, если все пойдет хорошо, тогда все ваши данные будут прочитаны, и вы можете утверждать на выходе.

Ответ 6

Мне удалось найти более простой способ. Тем не менее, вы должны использовать внешнюю библиотеку System.rules Автор @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());
  }
}

Проблема, однако, в моем случае мой второй вход имеет пробелы, и это делает весь поток ввода нулевым!

Ответ 7

Я исправил проблему с чтением из stdin, чтобы имитировать консоль...

Мои проблемы состояли в том, что я бы попытался написать в JUnit test консоль для создания определенного объекта...

Проблема в том, что вы говорите: как я могу писать в Stdin из теста JUnit?

Затем в колледже я узнаю о перенаправлениях, как вы говорите, что System.setIn(InputStream) меняет stdin filedescriptor, и вы можете писать тогда...

Но есть еще одна возможность исправить... тестовый блок JUnit ожидает чтения из вашего нового InputStream, поэтому вам нужно создать поток для чтения из InputStream и из теста JUnit Thread напишите в новом Stdin... Сначала вы должны писать в Stdin, потому что, если вы позже напишете о создании Thread для чтения из stdin, у вас наверняка будут условия гонки... вы можете писать в InputStream раньше, чтобы читать, или вы можете читать из InputStream до записи...

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

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