Недавно некоторые из моих студентов спросили меня о механизме модульного тестирования, предоставленном MOOC из Университета Хельсинки, я проверил их реализацию и думаю, что для начинающих было бы полезно понять, что произошло на самом деле, поэтому эта небольшая статья была опубликована.
В качестве примера мы будем использовать проект «Аэропорт» , это последнее задание за первую неделю ООП2.
Мы сосредоточены только на тестировании, поэтому я пропущу некоторые вопросы, касающиеся его решения. В этом упражнении мы каждый раз выполняли бы метод main
вручную, вводя идентификатор плоскости, емкость повторно, через некоторое время мы думаем, что наш код будет работать, мы запускаем локальные тесты, чтобы мы могли отправить их на сервер для онлайн-оценки и оценки.
Я использовал этот маленький проект в качестве примера рефакторинга с помощью защиты юнит-теста. Когда я многократно набираю номер самолета, номер рейса, код аэропорта и код операции, а также мучительно, я спрашиваю своих студентов: «Это больно или нет?».
Очевидно, все они ответили да. Тогда я спросил: «Будете ли вы снова и снова делать такого рода тесты, даже если это скучно и больно?».
Silence.
Из своего прошлого опыта я знаю, что эти скучные тесты легко пропустить, и мы можем успокоиться: «Этот код довольно прост, и я не могу ошибиться, он будет работать и будет работать, не волнуйтесь».
У меня были болезненные воспоминания из-за такого выбора, потому что я сделал слишком много простых и глупых ошибок в прошлом, поэтому, независимо от того, как это просто выглядит, я все равно буду делать тест — даже это мануальный тест, скучный и болезненный.
Я добавил это, потому что модульное тестирование не может полностью заменить ручное тестирование, хотя это сделает ручное тестирование более простым и эффективным.
Для проекта «Аэропорт», если нам не нужно вводить повторно каждый раз, и мы можем зафиксировать результаты нашей программы, по сравнению с тем, что ожидается, мы получим обратную связь намного быстрее.
1
2
3
|
String operation = scanner.nextLine(); ... System.out.println( "Blahblahblah..." ); |
Например, мы точно знаем, введем ли мы сначала x
, затем он перейдет к части «Обслуживание в полете» и напечатает варианты меню, если мы введем x
второй раз, программа завершит цикл и выйдет, в результате мы будем только получить вывод инструкций Панели Аэропорта и Службы полетов.
Итак, давайте перейдем к тестовому случаю, чтобы увидеть, что произойдет на самом деле.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
@Test public void printsMenusAndExits() throws Throwable { String syote = "x\nx\n" ; MockInOut io = new MockInOut(syote); suorita(f(syote)); String[] menuRivit = { "Airport panel" , "[1] Add airplane" , "[2] Add flight" , "[x] Exit" , "Flight service" , "[1] Print planes" , "[2] Print flights" , "[3] Print plane info" , "[x] Quit" }; String output = io.getOutput(); String op = output; for (String menuRivi : menuRivit) { int ind = op.indexOf(menuRivi); assertRight(menuRivi, syote, output, ind > - 1 ); op = op.substring(ind + 1 ); } } |
Выше 2-й тестовый случай, который охватывает самый простой сценарий, как мы уже сказали, введите только два x
.
Когда мы смотрим на тестовый код, он был разделен на 3 части:
- Подготовить вход
- выполнить
Main.main(args)
- Проверьте вывод, чтобы увидеть, содержит ли он все ожидаемые строки в последовательности
Вы знаете, что нормальное поведение scanner.nextLine()
или scanner.nextInt()
. Программа будет зависать и ждать ввода пользователя, так что будет выполнена следующая строка кода. Но почему здесь все идет гладко, без ожидания?
Прежде чем мы перейдем к этой части, я хочу кратко объяснить, как выполняется метод, он использует Java Reflection, чтобы вызвать метод не просто, но можно выполнить дополнительную проверку, например, в первом тестовом примере требуется, чтобы Main
был общедоступный класс, но вы, вероятно, обнаружите, что для прохождения ручного тестирования вы можете установить Main
уровень доступа для пакета.
1
2
3
4
5
|
@Test public void classIsPublic() { assertTrue( "Class " + klassName + " should be public, so it must be defined as\n" + "public class " + klassName + " {...\n}" , klass.isPublic()); } |
Здесь klass.isPublic()
проверяет, устанавливаете ли вы уровень доступа как требуется.
ХОРОШО. Кажется, что класс MockInOut
создает магию, мы можем проверить код, чтобы найти идею под капотом. Вы можете получить доступ к исходному коду на GitHub .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
public MockInOut(String input) { orig = System.out; irig = System.in; os = new ByteArrayOutputStream(); try { System.setOut( new PrintStream(os, false , charset.name())); } catch (UnsupportedEncodingException ex) { throw new RuntimeException(ex); } is = new ByteArrayInputStream(input.getBytes()); System.setIn(is); } |
Возможно, вы System.out
тысячи раз, но понимали ли вы, что вы можете изменить это молча, как описано выше? Здесь он указывает как на вход, так и на вход System, чтобы мы могли получить выходные данные полностью после выполнения, и нам не нужно вводить вручную в этот раз, потому что в операторе Scanner scanner = new Scanner(System.in);
параметр System.in
изменяется бесшумно, так что scanner.nextLine()
будет получать подготовленный ввод без зависания.
Кроме того, выходные данные не будут печататься в консоли, а накапливаются в ByteArrayOutputStream
, к которому можно получить доступ позже.
Вам может быть интересно, что если мы действительно хотим восстановить нормальное поведение System.in
и System.out
, что мы будем делать?
1
2
3
4
5
6
7
8
9
|
/** * Restores System.in and System.out */ public void close() { os = null ; is = null ; System.setOut(orig); System.setIn(irig); } |
По сути, он сохраняет исходные данные, когда требуется восстановление, просто очистите взломанные и установите их обратно, тогда все снова будет как обычно.
Вы можете скопировать простой пример кода ниже для быстрого тестирования.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
import java.io.*; import java.util.*; class HelloWorld { public static void main(String[] args) throws IOException { PrintStream orig = System.out; ByteArrayOutputStream os = new ByteArrayOutputStream(); System.setOut( new PrintStream(os, false , "UTF-8" )); // Here it won't print but just accumulate for ( int i = 0 ; i < 100 ; i++) { System.out.println( "Hello World" ); } System.setOut(orig); // Print 100 lines of "Hello World" here since out was restored System.out.println(os.toString( "UTF-8" )); InputStream is = System.in; System.setIn( new ByteArrayInputStream( "x\nx\n" .getBytes())); Scanner scanner = new Scanner(System.in); // Without hang on System.out.println(scanner.nextLine()); System.out.println(scanner.nextLine()); try { // There are only two lines provided, so here will fail System.out.println(scanner.nextLine()); } catch (NoSuchElementException e) { e.printStackTrace(); } System.setIn(is); scanner = new Scanner(System.in); // Hang on here since `in` was restored System.out.println(scanner.nextLine()); } } |
Фактически, внедрение и замена — это часто используемый метод для разделения зависимостей для модульных тестов, который весьма полезен, чтобы сосредоточиться только на вашем коде. Есть более продвинутые и сложные подходы, чтобы сделать это, но здесь мы просто хотим объяснить простой подход, который «взламывает» и делает так, чтобы вы могли сосредоточиться на своем коде, а не на in
и out
.
Для некоторых устаревших проектов этот метод может быть критичным для рефакторинга, так как слишком много тяжелых зависимостей делают тестирование действительно трудным!
Опубликовано на Java Code Geeks с разрешения Натанаэля Янга, партнера нашей программы JCG . См. Оригинальную статью здесь: простой подход к симуляции пользовательского ввода и проверки вывода Мнения, высказанные участниками Java Code Geeks, являются их собственными. |