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
Не трябва да тествате класа Scanner на Java. Можете ръчно да зададете входа си и просто да тествате собствената си логика. 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 как да направя това с множество входове? Кажете, че в //do your thing имам три подкани за потребителско въвеждане. Как да направя това? - person Stupid.Fat.Cat; 14.05.2014
comment
@Stupid.Fat.Cat Добавих втори подход към проблема, по-елегантен и гъвкав - person KrzyH; 14.05.2014
comment
@KrzyH За ​​втория подход ще трябва да предадете входния поток и изходния поток като параметър в дефиницията на метода, което не търся. Знаете ли за някакъв по-добър начин? - person Chirag; 01.10.2015
comment
Как да симулирам натискане на клавиша enter? В момента програмата просто чете целия вход наведнъж, което не е добре. Опитах \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

Един често срещан начин за тестване на подобен код би бил извличането на метод, който приема скенер и 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() извиква method(in, out), а не processUserInput(in, out)? - 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

Поправих проблема с четенето от stdin за симулиране на конзола...

Проблемът ми беше, че бих искал да опитам да напиша в JUnit, да тествам конзолата, за да създам определен обект...

Проблемът е като всичко, което казвате: Как мога да пиша в Stdin от теста JUnit?

След това в колежа научавам за пренасочвания като вие казвате System.setIn(InputStream) променя stdin файловия дескриптор и можете да пишете след това...

Но има още един проблем за коригиране... тестовият блок JUnit чака четене от вашия нов InputStream, така че трябва да създадете нишка, която да чете от InputStream и от JUnit test Thread запис в новия Stdin... Първо трябва да пишете в Stdin, защото ако пишете по-късно или създадете нишката за четене от 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();      
}
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