Тестирование 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? Нравится имитация пользовательского ввода?

Заранее спасибо.


person Wimpey    schedule 20.06.2011    source источник
comment
Что именно вы тестируете? Сканер? Метод тестирования обычно должен утверждать что-то полезное.   -  person Markus    schedule 20.06.2011
comment
Вам не нужно тестировать класс Java Scanner. Вы можете вручную установить ввод и просто проверить собственную логику. int input = -1 или 5 или 11 покроет вашу логику   -  person blong824    schedule 20.06.2011
comment
Шесть лет спустя... и это все еще хороший вопрос. Не в последнюю очередь потому, что, приступая к разработке приложения, вы, как правило, не хотите иметь все навороты JavaFX, а вместо этого просто начинаете использовать скромную командную строку для небольшого количества очень простого взаимодействия. Жаль, что JUnit не делает это немного проще. Для меня ответ Омара Эльшала очень хорош, с минимальным надуманным или искаженным кодированием приложения...   -  person mike rodent    schedule 04.06.2017


Ответы (7)


Вы можете заменить System.in с вашим собственным потоком, вызвав System.setIn(InputStream 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;
}
person KrzyH    schedule 20.06.2011
comment
@KrzyH, как мне сделать это с несколькими входами? Скажем, в //делай свое дело у меня есть три подсказки для пользовательского ввода. Как бы я это сделал? - person Stupid.Fat.Cat; 14.05.2014
comment
@Stupid.Fat.Cat Я добавил второй подход к проблеме, более элегантный и удобный - person KrzyH; 14.05.2014
comment
@KrzyH Для второго подхода вам потребуется передать входной поток и выходной поток в качестве параметра в определении метода, чего я не ищу. Вы знаете какой-нибудь лучший способ? - person Chirag; 01.10.2015
comment
Как имитировать нажатие клавиши ввода? Прямо сейчас программа просто считывает весь ввод за один раз, что нехорошо. Я пробовал \n, но это не имеет значения - person CodyBugstein; 19.10.2015
comment
@Imray: Вы решили эту проблему? у меня вот так же. - person t777; 11.12.2015
comment
@ t777 нет :( - person CodyBugstein; 11.12.2015
comment
@CodyBugstein Используйте ByteArrayInputStream in = new ByteArrayInputStream(("1" + System.lineSeparator() + "2").getBytes());, чтобы получить несколько входных данных для разных вызовов keyboard.nextLine(). - person A Jar of Clay; 02.04.2019

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

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

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

person Garrett Hall    schedule 20.06.2011

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

person Jeff Bowman    schedule 29.04.2014
comment
Вы имели в виду для тестирования, метод processUserInput() вызывает метод (вход, выход), а не processUserInput(вход, выход)? - person NadZ; 21.12.2016
comment
@NadZ Конечно; Я начал с общего примера и не полностью изменил его, вернув его к конкретному вопросу. - person Jeff Bowman; 21.12.2016

Мне удалось найти более простой способ. Однако вам необходимо использовать внешнюю библиотеку 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;
  }
}

Контрольная работа

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

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

person Omar Elshal    schedule 15.11.2016
comment
Это работает очень хорошо... Я не знаю, что вы подразумеваете под нулевым входным потоком. Что хорошо, так это то, что inputEntry = scanner.nextLine(); при использовании с System.in всегда будет ждать пользователя (и примет пустую строку как пустую String)... тогда как, когда вы вводите строки, используя systemInMock.provideLines(), это выдаст NoSuchElementException, когда строки закончатся. Это действительно позволяет не слишком сильно искажать код приложения для тестирования. - person mike rodent; 01.06.2017
comment
Я точно не помню в чем была проблема, но вы правы, я еще раз проверил свой код и заметил, что исправил его двумя способами: 1. Используя systemInMock: systemInMock.provideLines("1", "2"); 2. Используя System.setIn без внешних библиотек: String data2 = "1 2"; System.setIn(new ByteArrayInputStream(data2.getBytes())); - person Omar Elshal; 02.06.2017

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

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

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

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

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

Это мой код, у меня плохое знание английского языка. Я надеюсь, что все вы можете понять проблему и решение для имитации записи на стандартный ввод из теста 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();      
}
person Javier Gutiérrez-Maturana Sánc    schedule 26.03.2017

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

person Sarah Haskins    schedule 20.06.2011
comment
Обратите внимание, что вы не можете имитировать объекты Scanner (они являются окончательными). - person hoipolloi; 20.06.2011

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

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

person massfords    schedule 20.06.2011