Тестування JUnit із змодельованим введенням користувачем


82

Я намагаюся створити деякі тести 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? Як симуляція вводу користувача?

Заздалегідь спасибі.


7
Що саме ви тестуєте? Сканер? Тестовий метод, як правило, повинен стверджувати, що є корисним.
Маркус

2
Вам не доведеться тестувати клас сканера Java. Ви можете вручну встановити введення та просто перевірити власну логіку. int input = -1 або 5 або 11 буде охоплювати вашу логіку
blong824

3
Шість років потому ... і це все ще гарне питання. Не в останню чергу тому, що, починаючи розробку програми, ви можете, як правило, не хотіти мати усіх примх JavaFX, а натомість просто починайте використовувати покірний командний рядок для трохи базової взаємодії. Соромно, що JUnit не робить це трохи простішим. Для мене відповідь Омара Ельшаля дуже приємна, з мінімальним "надуманим" або "викривленим" кодуванням програми ...
Майк Гризун

Відповіді:


105

Ви можете замінити System.in власним потоком, зателефонувавши System.setIn (InputStream в) . 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;
}

6
@KrzyH як би я зробив це з кількома входами? Скажіть, що в // робіть те, що у мене є три підказки для введення користувачем. Як би я про це пішов?
Stupid.Fat.Cat

1
@ Stupid.Fat.Cat Я додав другий підхід до проблеми, більш елегантний і зручний
KrzyH

1
@KrzyH Для другого підходу вам потрібно буде передати вхідний потік та вихідний потік як параметр у визначенні методу, що я не шукаю. Чи знаєте ви якийсь кращий спосіб?
Chirag

6
Як імітувати натискання клавіші Enter? Зараз програма просто зчитує весь вхід за один постріл, що не годиться. Я намагався, \nале це не має значення
КодіБугштайн

3
@CodyBugstein Використовуйте ByteArrayInputStream in = new ByteArrayInputStream(("1" + System.lineSeparator() + "2").getBytes());для отримання декількох входів для різних keyboard.nextLine()дзвінків.
Jar of Clay

18

Щоб перевірити код вашого коду, вам слід створити обгортку для системних функцій введення / виводу. Ви можете зробити це, використовуючи введення залежностей, давши нам клас, який може запитувати нові цілі числа:

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

Це може здатися надмірним для вашого маленького прикладу, але якщо ви створюєте більший додаток, що розвивається таким чином, це може швидко окупитися.


6

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


Ви призначені для тестування, метод processUserInput () викликає метод (in, out), а не processUserInput (in, out)?
NadZ

@NadZ Звичайно; Я розпочав із загального прикладу і повністю змінив його на конкретний для питання.
Джефф Боуман

4

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

Проблема, однак, у моєму випадку у мого другого введення є пробіли, і це робить весь вхідний потік нульовим!


Це працює дуже добре ... Я не знаю, що ви маєте на увазі під "вхідним потоком null". Хорошим є те, що inputEntry = scanner.nextLine();при використанні System.inзавжди буде чекати користувача (і буде приймати порожній рядок як порожній String) ... тоді як, коли ви надаєте рядки, systemInMock.provideLines()це буде кидати, NoSuchElementExceptionколи рядки закінчуються. Це дійсно полегшує не занадто "спотворювати" код програми, щоб задовольнити потреби тестування.
гризун мікрофона

Я точно не пам’ятаю, в чому була проблема, але ви маєте рацію, я ще раз перевірив свій код і помітив, що виправив його двома способами: 1. Використання systemInMock: systemInMock.provideLines("1", "2"); 2. Використання System.setIn без зовнішніх бібліотек:String data2 = "1 2"; System.setIn(new ByteArrayInputStream(data2.getBytes()));
Омар Ельшаль

2

Ви можете почати з вилучення логіки, яка отримує номер із клавіатури, у свій власний метод. Тоді ви можете перевірити логіку перевірки, не турбуючись про клавіатуру. Для тестування виклику keyboard.nextInt () ви можете розглянути можливість використання макетного об’єкта.


2
Зауважте, ви не можете знущатися над об’єктами Сканера (вони остаточні).
hoipolloi

2

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

Мої проблеми полягали в тому, що я хотів би спробувати написати в JUnit перевірити консоль, щоб створити певний об'єкт ...

Проблема схожа на все, що ви говорите: Як я можу написати в тесті Stdin з JUnit?

Потім у коледжі я дізнаюся про переспрямування, як ви кажете System.setIn (InputStream), змініть stdin вкладений дескриптор, і ви можете написати тоді ...

Але є ще одна проблема, яку можна виправити ... Тестовий блок JUnit чекає читання з вашого нового InputStream, тому вам потрібно створити потік для читання з InputStream та з тестового JUnit Запис потоку в новому Stdin ... Спочатку вам потрібно пишіть у Stdin, тому що якщо ви пізніше пишете про create 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();      
}

1

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

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.