КулЛиб электронная библиотека 

Дружеское знакомство с тестированием программ [Билл Лабун] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


Настройки текста:



rescuer

Билл Лабун

Санкт-Петербург
«БХВ-Петербург»
2022

УДК 004.415.53
ББК 32.972
Л12

Лабун Б.
Л12

Дружеское знакомство с тестированием программ: Пер. с англ. — СПб.:
БХВ-Петербург, 2022. — 288 с.: ил.
ISBN 978-5-9775-6807-4
Рассмотрены основные понятия и терминология в сфере тестирования и контроля качества ПО. Приведены рекомендации по составлению правил тестирования и отчетов об обнаруженных дефектах. Описано тестирование производительности, безопасности, комбинаторное тестирование. Подробно рассмотрены классы
эквивалентности, граничные случаи, угловые случаи, статическое и динамическое
тестирование. Даны сведения о проведении приемочного и исследовательского
тестирования, описаны средства автоматизации. Отдельные разделы посвящены
юнит-тестированию, разработке через тестирование, попарному и комбинаторному, стохастическому тестированию и тестированию на основе свойств.

Для начинающих тестировщиков ПО
УДК 004.415.53
ББК 32.972
Группа подготовки издания:
Руководитель проекта
Зав. редакцией
Перевод с английского
Компьютерная верстка
Оформление обложки

Павел Шалин
Людмила Гауль
Игоря Донченко
Ольги Сергиенко
Карины Соловьевой

Copyright © 2021 by Bill Laboon
Translation Copyright © 2021 by BHV. All rights reserved.
Перевод © 2021 BHV. Все права защищены.

"БХВ-Петербург", 191036, Санкт-Петербург, Гончарная ул., 20.

ISBN 978-1-523-47737-1 (англ.)
ISBN 978-5-9775-6807-4 (рус.)

© Bill Laboon, 2021
© Перевод на русский язык, оформление.
ООО "БХВ-Петербург", ООО "БХВ", 2021

Оглавление

Глава 1. Введение .......................................................................................................... 11
1.1. История вопроса ..................................................................................................................... 11
1.2. Тестирование и обеспечение качества .................................................................................. 11
1.3. Что вы найдете в этой книге .................................................................................................. 12
1.4. Чего нет в этой книге.............................................................................................................. 13
1.5. Замечание по выбору языка программирования .................................................................. 13
Глава 2. Что такое тестирование программного обеспечения? ........................... 14
2.1. Определение тестирования программного обеспечения ..................................................... 14
2.2. Верификация и валидация...................................................................................................... 15
2.3. Предварительное определение дефекта ................................................................................ 16
2.4. Пример тестирования в реальной жизни .............................................................................. 18
Глава 3. Зачем тестировать программы?................................................................. 21
3.1. Тестировать или не тестировать ............................................................................................ 21
3.2. Ни один из разработчиков не совершенен ........................................................................... 22
3.3. Обнаружить дефекты раньше, чем позже ............................................................................. 22
3.4. Стабильность........................................................................................................................... 23
3.5. Защита пользователя .............................................................................................................. 23
3.6. Независимый взгляд на всю систему .................................................................................... 24
3.7. Обеспечивая качество ............................................................................................................ 24
3.8. Риск .......................................................................................................................................... 25
Глава 4. Основы тестирования .................................................................................. 26
4.1. Классы эквивалентности и поведение .................................................................................. 26
4.2. Внутренние и граничные значения ....................................................................................... 28
4.3. Базовые случаи, граничные случаи, угловые случаи ........................................................... 31
4.4. Успешные и неуспешные случаи .......................................................................................... 32
4.5. Тестирование черного, белого и серого ящиков .................................................................. 32
4.6. Статическое и динамическое тестирование ......................................................................... 34
Глава 5. Требования ..................................................................................................... 36
5.1. Тестируемость......................................................................................................................... 39
5.2. Функциональное против нефункционального...................................................................... 42
5.3. Замечание о наименовании требований................................................................................ 43

6

Оглавление

Глава 6. Тест-планы ..................................................................................................... 44
6.1. Базовая схема тест-плана ....................................................................................................... 44
6.1.1. Идентификатор ............................................................................................................ 45
6.1.2. Тест-кейс (или краткое изложение)............................................................................ 46
6.1.3. Предусловия ................................................................................................................. 46
6.1.4. Входные значения ........................................................................................................ 48
6.1.5. Шаги выполнения ........................................................................................................49
6.1.6. Выходные значения .....................................................................................................51
6.1.7. Постусловия ................................................................................................................. 51
6.1.8. Ожидаемое поведение и наблюдаемое поведение .................................................... 51
6.2. Разработка тест-плана ............................................................................................................ 52
6.3. Тестовые фикстуры ................................................................................................................ 54
6.4. Выполнение тест-плана .......................................................................................................... 55
6.5. Отслеживание тестовых прогонов ........................................................................................ 57
6.6. Матрицы трассируемости ...................................................................................................... 59
Глава 7. Ломая программу .......................................................................................... 62
7.1. Ошибки, которые следует искать ..........................................................................................62
7.2. Список продолжается и продолжается ................................................................................. 73
Глава 8. Прохождение тестового плана .................................................................... 74
8.1. Изучение требований ............................................................................................................. 74
8.2. Разрабатывая тест-план .......................................................................................................... 76
8.3. Заполняя тест-план ................................................................................................................. 77
8.4. Определяя фокус ..................................................................................................................... 80
8.5. Тест-кейсы для нефункционального требования ................................................................. 84
Глава 9. Дефекты........................................................................................................... 85
9.1. Что такое дефект? ................................................................................................................... 85
9.2. Жизненный цикл дефекта ...................................................................................................... 86
9.3. Стандартизованный шаблон дефекта .................................................................................... 89
9.3.1. Краткое описание......................................................................................................... 89
9.3.2. Описание....................................................................................................................... 90
9.3.3. Шаги воспроизведения ................................................................................................ 90
9.3.4. Ожидаемое поведение ................................................................................................. 91
9.3.5. Наблюдаемое поведение ............................................................................................. 92
9.3.6. Влияние......................................................................................................................... 92
9.3.7. Серьезность .................................................................................................................. 93
9.3.8. Решение ........................................................................................................................ 94
9.3.9. Заметки ......................................................................................................................... 95
9.4. Исключения в шаблоне .......................................................................................................... 95
9.5. Примеры дефектов ................................................................................................................. 96
Глава 10. Дымовое и приемочное тестирование ..................................................... 99
10.1. Дымовое тестирование ......................................................................................................... 99
10.2. Приемочное тестирование .................................................................................................101
Глава 11. Исследовательское тестирование .......................................................... 104
11.1. Преимущества и недостатки исследовательского тестирования .................................... 104
11.2. Руководство по исследовательскому тестированию ....................................................... 106

Оглавление

7

Глава 12. Ручное тестирование против автоматизированного
тестирования ................................................................................................................ 109
12.1. Преимущества и недостатки ручного тестирования ........................................................ 109
12.1.1. Преимущества ручного тестирования .................................................................... 109
12.1.2. Недостатки ручного тестирования ......................................................................... 111
12.2. Преимущества и недостатки автоматизированного тестирования ................................. 112
12.2.1. Преимущества автоматизированного тестирования ............................................. 112
12.2.2. Недостатки автоматизированного тестирования .................................................. 114
12.3. Реальный мир ...................................................................................................................... 115
Глава 13. Введение в юнит-тестирование .............................................................. 117
13.1. Юнит-тестирование: сама идея ......................................................................................... 117
13.2. Пример на естественном языке ......................................................................................... 120
13.3. Превратим наш пример в юнит-тест ................................................................................. 121
13.3.1. Предусловия ...........................................................................................................122
13.3.2. Шаги выполнения................................................................................................... 123
13.3.3. Утверждения ...........................................................................................................123
13.3.4. Обеспечение проверки тестами того, что вы ожидаете ...................................... 124
13.4. Проблемы с юнит-тестированием ..................................................................................... 126
13.5. Создание тест-раннера ....................................................................................................... 128
Глава 14. Продвинутое юнит-тестирование .......................................................... 131
14.1. Тестовые двойники ............................................................................................................. 131
14.2. Заглушки.............................................................................................................................. 135
14.3. Моки и верификация .......................................................................................................... 136
14.4. Фейки ................................................................................................................................... 139
14.5. setup() и tearDown() ............................................................................................................ 140
14.6. Тестирование системного вывода ..................................................................................... 142
14.7. Тестирование private-методов ........................................................................................... 143
14.8. Структура юнит-теста......................................................................................................... 145
14.8.1. Основной план ........................................................................................................145
14.8.2. Что тестировать? .................................................................................................... 145
14.8.3. Утверждайте меньше, называйте прямо............................................................... 146
14.8.4. Юнит-тесты должны быть независимыми ........................................................... 146
14.8.5. Старайтесь сделать тесты лучше каждый раз, когда вы их касаетесь ............... 149
14.9. Покрытие кода .................................................................................................................... 149
Глава 15. Разработка через тестирование .............................................................. 153
15.1. Что такое разработка через тестирование? ...................................................................... 153
15.2. Цикл "красный — зеленый — рефакторинг" ................................................................... 156
15.3. Принципы разработки через тестирование ...................................................................... 158
15.4. Пример: создание программы FizzBuzz с использованием разработки
через тестирование ............................................................................................................. 159
15.5. Преимущества TDD ............................................................................................................ 163
15.6. Недостатки TDD ................................................................................................................. 165

Глава 16. Написание тестируемого кода ................................................................ 167
16.1. Что мы понимаем под тестируемым кодом? .................................................................... 167
16.2. Основные стратегии тестируемого кода ........................................................................... 168

8

Оглавление

16.3. Предусмотрите сценарный интерфейс.............................................................................. 171
16.4. Написание тестов заранее .................................................................................................. 172
16.5. Пусть ваш код будет DRY.................................................................................................. 172
16.6. Внедрение зависимости ..................................................................................................... 174
16.7. Недружественные к тестированию функции и конструкции .......................................... 175
16.8. Работа с чужим унаследованным кодом ........................................................................... 176
16.9. Заключительные мысли о написании тестируемого кода ............................................... 179

Глава 17. Попарное и комбинаторное тестирование ........................................... 180
17.1. Перестановки и комбинации..............................................................................................182
17.2. Попарное тестирование ...................................................................................................... 184
17.3. n-сторонние взаимодействия ............................................................................................. 188
17.4. Работа с большими наборами переменных ...................................................................... 189
Глава 18. Стохастическое тестирование и тестирование
на основе свойств ........................................................................................................ 191
18.1. Бесконечные обезьяны и бесконечные пишущие машинки............................................ 192
18.2. Тестирование на основе свойств ....................................................................................... 192
18.2.1. Взбираясь по лестнице абстракции ...................................................................... 193
18.3. Умные, тупые, злые и хаотические обезьяны .................................................................. 194
18.4. Мутационное тестирование ...............................................................................................197
Глава 19. Тестирование производительности ....................................................... 202
19.1. Категории показателей производительности ................................................................... 203
19.2. Тестирование производительности: пределы и цели....................................................... 204
19.3. Ключевые показатели производительности ..................................................................... 205
19.4. Тестирование показателей, ориентированных на сервис: время отклика ..................... 206
19.4.1. Что такое время? .................................................................................................... 207
19.4.2. Какие события следует измерять? ........................................................................ 209
19.5. Тестирование показателей, ориентированных на сервис: доступность ......................... 210
19.6. Тестирование показателей, ориентированных на эффективность:
пропускная способность..................................................................................................... 215
19.7. Тестирование показателей, ориентированных на эффективность: утилизация ............ 217
19.8. Общие советы и рекомендации для нагрузочного тестирования ................................... 218

Глава 20. Тестирование безопасности ..................................................................... 220
20.1. Вызовы в тестировании безопасности .............................................................................. 221
20.2. Основные концепции компьютерной безопасности ........................................................ 223
20.3. Распространенные атаки и как использовать тестирование против них ....................... 226
20.3.1. Инъекция ................................................................................................................. 226
20.3.2. Переполнение буфера ............................................................................................ 228
20.3.3. Неправильная настройка безопасности ................................................................ 229
20.3.4. Небезопасное хранение.......................................................................................... 229
20.3.5. Социальная инженерия .......................................................................................... 230
20.4. Тестирование на проникновение ....................................................................................... 232
20.5. Общие рекомендации ......................................................................................................... 233
Глава 21. Взаимодействие с заинтересованными лицами .................................. 234
21.1. Кто такие заинтересованные лица? ................................................................................... 234
21.2. Отчеты и общение .............................................................................................................. 236

Оглавление

9

21.3. Красно-желто-зеленый шаблон ......................................................................................... 239
21.4. Отчеты о состоянии ............................................................................................................ 242
21.4.1. Пример: ежедневный отчет о состоянии дел для тестировщика
программного обеспечения ............................................................................................... 242
21.5. Замечание об управлении ожиданиями ............................................................................ 243
21.6. Замечание о встречах ......................................................................................................... 244
21.7. Разъяснение требований..................................................................................................... 244
21.8. Этические обязательства .................................................................................................... 245
21.9. Уважение ............................................................................................................................. 246

Глава 22. Заключение ................................................................................................. 248
Глава 23. Шаблоны тестирования ........................................................................... 249
23.1. Шаблон тест-кейса.............................................................................................................. 249
23.2. Шаблон отчета о дефектах ................................................................................................. 249
23.3. Красно-желто-зеленый шаблон ......................................................................................... 250
23.4. Ежедневный отчет о состоянии ......................................................................................... 250
Глава 24. Использование рефлексии для тестирования private-методов
в Java .............................................................................................................................. 251
Глава 25. Что еще почитать ...................................................................................... 256
Глава 26. Словарь терминов ..................................................................................... 259
Глава 27. Благодарности............................................................................................ 281
Предметный указатель .............................................................................................. 283

ГЛАВА 1

Введение
Признаюсь, я редко читаю вступления к техническим книгам, и поэтому не обижусь, если вы не станете читать это.

1.1. История вопроса
Давным-давно в одной далекой компании я работал ведущим тестировщиком (тестлидом). Одной из моих обязанностей было проведение собеседований с кандидатами на должность инженера по тестированию в нашу команду, и я понял, что у многих из них нет необходимого понимания тестирования. Те, кто добился какого-то
карьерного успеха, зачастую набирались опыта сами по себе по ходу работы. Даже
те, у кого было образование в области информационных (или сопутствующих) технологий, зачастую не знали о тестировании программного обеспечения (ПО). Разработчики учились тестированию своего кода, используя подход "новичка" и спрашивая старших разработчиков, что именно необходимо протестировать.
Я понял, что могу либо жаловаться на это, либо попытаться что-то сделать. Мне
удалось убедить руководство кафедры вычислительной техники (Computer Science
department) Университета Питтсбурга позволить мне разработать и преподавать
курс по тестированию программного обеспечения. Этот маленький курс вырос,
стал раскрывать многие аспекты качества ПО и в итоге оказался очень популярным, заняв свое место среди дисциплин университета (CS 1632: Software Quality
Assurance, если вы захотите принять участие!). Мне пришлось разработать свой
учебный план, т. к. мне не удалось найти хорошую книгу или конспект, в которых
был бы выдержан баланс между теорией и практикой. Отыскать хорошую книгу
оказалось даже более сложной задачей, чем выбрать при собеседовании хорошего
инженера по обеспечению качества (Quality Assurance, QA)! И снова я понял, что
могу либо жаловаться на это, либо попытаться что-то сделать, и я выбрал последнее.

1.2. Тестирование и обеспечение качества
Тестирование является важной частью процесса разработки ПО и полезно не только для тех, кто собирается сделать карьеру в области QA. Разработчик, не заботя-

12

Глава 1

щийся о качестве ПО, не хороший разработчик. Эта книга предназначена для тех,
кто интересуется тестированием ПО или написанием тестов как разработчик.

1.3. Что вы найдете в этой книге
Целью этой книги является сравнительно достаточный обзор тестирования ПО.
Я надеюсь, что после ее прочтения у читателя будут все знания и навыки, необходимые для включения в процесс обеспечения качества, и я также рассчитываю на
то, что менеджеры, программисты и все, кто связан с разработкой ПО, могут найти
ее интересной.
Именно поэтому книга начинается с общего описания предметной области — что
такое тестирование программного обеспечения, в конце концов?! Довольно сложно
изучать предмет без понимания, что это! Затем мы перейдем к теории и терминологии, используемым теми, кто работает в индустрии тестирования ПО. Открою вам
маленький секрет — пожалуй, это наименее интересная часть книги. Тем не менее
мы должны говорить на одном языке при обсуждении основ. Наверное, было бы
трудно объяснить принцип работы водопровода, если бы нам пришлось отказаться
от использования слова "труба" из опасения, что кто-то не понимает его.
Далее мы перейдем к основам разработки ручных тест-планов и к обработке дефектов, найденных в процессе их выполнения. Ручные тесты сейчас используются
гораздо меньше, чем раньше; автоматизированные тесты освободили нас от скуки,
связанной с их выполнением. Тем не менее существуют определенные преимущества разработки тестов без необходимости беспокоиться о синтаксисе какого-либо
языка программирования или наборе инструментов. Мы также посмотрим, как правильно описывать наше тестирование и сообщать о дефектах, найденных во время
его проведения.
Так как ручное тестирование несколько устарело, мы перейдем к автоматизированным тестам — тестам системного уровня и юнит-тестам. Автоматизация позволяет
вам выполнять тесты очень быстро как на низком уровне (например, позволяя убедиться, что алгоритм сортировки работает правильно), так и на высоком (путем добавления чего-либо в корзину вашего интернет-магазина). Если вы когда-либо занимались выполнением ручных тест-планов, вы поймете, что передача компьютеру
выполнения всех ваших тестов освободит для вас немало времени. Но, пожалуй,
гораздо более важно то, что это также освободит вас и от гнева, т. к. постоянное
выполнение ручных тестов является самым быстрым способом вывести тестировщика из себя.
И в итоге мы придем к по-настоящему интересным вещам! Это та часть книги,
в которой вы прочитаете о таких специализированных видах тестирования, как
комбинаторное тестирование, тестирование производительности и тесты безопасности. Мир тестирования ПО довольно большой, и тестирование встроенного ПО
сильно отличается от тестирования веб-приложений, тестирование производительности отличается от функционального тестирования, а тестирование первого про-

Введение

13

дукта-прототипа, выпущенного стартапом, не похоже на тестирование медицинского устройства, выпускаемого большой компанией.

1.4. Чего нет в этой книге
Эта книга — ознакомительный текст о мире тестирования; и вам откроются двери
в огромное пространство для исследований, если вы решите углубиться в рассматриваемые здесь вопросы. Однако нашей целью является не соревнование с "Войной
и миром" в объеме, а общее введение в тематику. Потратить много времени на
сложные разделы каждого из вопросов может быть интересно мне и еще ограниченному количеству читателей книги, но моими целями являются создание хорошего основания для знаний по практическому тестированию ПО и знакомство
с некоторыми особенностями и аспектами этой индустрии. Рассматривайте книгу
как обзорную экскурсию по Европе, а не год жизни в Париже.

1.5. Замечание по выбору языка
программирования
Примеры, которые я привожу, написаны на языке программирования Java и используют связанные с ним инструменты (например, JUnit). Это не связано с какойто особенной любовью к Java. Существуют другие языки, о которых говорят, что
они легче в использовании или более удобны для минимизации количества дефектов. Но Java сегодня является lingua franca1 для разработчиков. Это стандартный,
популярный и много почерпнувший из ALGOL язык программирования, и даже
если вы не очень знакомы с ним, вы, возможно, сможете разобраться в том, что мы
будем делать.

1

Универсальным языком. — Прим. ред.

ГЛАВА 2

Что такое
тестирование программного обеспечения?
Начнем с того, чем оно не является.
1. Это не поиск всех без исключения дефектов.
2. Это не случайные нажатия по клавиатуре в надежде, что что-то сломается.
3. Даже не надейтесь, что что-то сломается. Точка.
4. Это не то, чем начинают заниматься после завершения программирования.
5. Это совсем, СОВСЕМ не то, что можно отложить до того момента, как пользователи начнут жаловаться.

2.1. Определение тестирования
программного обеспечения
На высоком уровне тестирование ПО является способом предоставления оценки
качества программного обеспечения заинтересованным лицам (т. е. людям, напрямую заинтересованным в работе системы, — потребителям, пользователям и
менеджерам). Хотя эти лица могут напрямую не беспокоиться о качестве ПО, они
заинтересованы в управлении риском. Данный риск может принимать разнообразные формы. Для покупателей использование ПО несет риски потери данных, риски
прекращения деятельность, риски того, что использование ПО принесет больше
убытков, чем прибыли. Для внутренних заинтересованных лиц риски могут включать задержки с релизом ПО (или его невыходом вообще), потерей клиентов из-за
выпуска некачественного ПО или даже судебные иски. Благодаря тестированию
ПО вы сможете лучше оценить, какие риски несут заинтересованные лица.
Тестирование ПО позволяет непредвзято взглянуть на программный продукт. Разработчики известны тем, что — осознанно или неосознанно — относятся довольно
просто к коду, который они пишут. Хотел бы я сказать, что, когда сам пишу код,
избегаю этого и фокусируюсь на качестве ПО. Тем не менее зачастую мое общение
с тем, кто тестирует мою программу, проходит примерно так:
Я: "Моя процедура вычисления квадратного корня работает! И работает в два
раза быстрее, чем раньше!"

Что такое тестирование программного обеспечения?

15

Тестировщик: "Хм... Когда я ввожу букву вместо числа, программа перестает
работать".
Я: "О, я уверен, что никто не будет вводить буквы".
Тестировщик: "Когда я ввожу отрицательное число, программа перестает работать".
Я: "Да, мы не поддерживаем работу с комплексными числами. Поэтому я не делаю такую проверку".
Тестировщик: "Я ввожу 2.0 и получаю сообщение об ошибке, что программа не
может работать с дробными числами".
Я: "Да, по идее, программа должна работать, но в данный момент она принимает
в качестве данных только целые числа. Но пользователь должен знать об этом".
Тестировщик: "Хорошо, когда я ввожу 2, экран заполняется цифрами, идущими
после запятой..."
Я: "Да, конечно! Квадратный корень двойки является иррациональным числом,
поэтому расчет будет идти до тех пор, пока существует Вселенная! Просто введи любое положительное число, возведенное в квадрат".
Тестировщик: "Когда я печатаю 25, то программа выдает мне 3".
Я: "Да, пожалуй, здесь ошибка. Я тестировал программу только с 9, и она прошла все мои тесты!"
(...И так далее.)
Помните, что я тот, кто читает лекции по тестированию ПО. И даже я не могу не
любить эти маленькие бедные функции, которые пишу. И работа тестировщика заключается в том, чтобы выбить из меня эту родительскую любовь. Я могу вырастить свою маленькую бедную функцию, но не могу предсказать, как она поведет
себя, оказавшись в сложной ситуации.

2.2. Верификация и валидация
Тестирование также должно гарантировать, что создано программное обеспечение
требуемого качества. Представьте себе такой разговор между менеджером проекта
и пользователем.
Менеджер проекта: "Я прошелся по всему проекту. Криптографический движок
просто пуленепробиваемый, рекордно быстрый и использует 8192-битное кодирование — ваши секреты будут в безопасности триллион лет".
Пользователь: "На самом деле, я всего лишь хотел поиграть в пасьянс..."
Можно ли сказать, что программа удовлетворяет требованиям пользователя? Конечно, нет. Даже если программа удовлетворяет всем предъявляемым к программам требованиям, не падает, дает правильные ответы и т. д., но при этом не отвечает ожиданиям пользователя, она не будет успешной.

16

Глава 2

Это иллюстрирует разницу между верификацией и валидацией. Верификация
гарантирует, что вы создаете программу правильно; валидация гарантирует, что вы
создаете правильную программу. Другими словами, верификация гарантирует, что
система не ломается, она отвечает необходимым требованиям, обрабатывает ошибки корректно и т. д. Валидация обеспечивает соответствие требований реальным
ожиданиям потребителя: выполняет ли программа то, что нужно пользователю?
Существуют ли какие-то недочеты в требованиях, ведущие к тому, что даже если
программа удовлетворяет им всем, пользователь все рано останется недовольным?
И верификация, и валидация являются частью процесса тестирования ПО. Хотя
большинство тестировщиков тратят бóльшую часть своего времени на верификацию, тестировщик не относится формально к тому, что программа соответствует
требованиям. Тестировщиков можно представить как защитников интересов пользователя, даже если это противоречит интересам других заинтересованных лиц —
необходимо создавать продукт, который соответствует пожеланиям пользователей,
а не просто пытаться выпустить программу.
Интересно, что чем больше времени и ресурсов тратится на исправление дефектов
или улучшение качества каким-либо образом, тем больше средств будет сэкономлено в долгосрочной перспективе. Работа с системой, в которой мало дефектов или,
по крайней мере, существуют дефекты, о которых вы знаете, окажется значительно
более легкой, чем когда вы захотите добавить новые функции в программу, которая
периодически перестает работать и делает это как будто случайным образом. Система, проверяемая хорошим набором автоматизированных тестов, позволит вам
вносить изменения и быть уверенным, что вы не создали новые дефекты в процессе
разработки. Это парадокс качества программного обеспечения — вы можете потратить меньше денег и времени на создание лучшего продукта путем правильного
тестирования.

2.3. Предварительное определение дефекта
Важно понимать, что не всякая найденная в системе проблема становится дефектом. Дефект является проблемой, которая либо нарушает функциональность системы в ее текущем понимании, либо не соответствует требованиям программы. Если
программа работает нормально и соответствует всем требованиям, значит, у нее нет
дефектов. Если программа не соответствует требованиям или не работает нормально, значит, можно говорить о том, что обнаружен дефект.
Например, рассмотрим компанию, создающую совершенно новую версию игры
"крестики-нолики". Требования следующие:
1. Игровая доска должна быть три на три квадрата, всего девять смежных квадратов.
2. Первый игрок может поставить значок "Х" (крестик) в один любой квадрат.
3. Второй игрок может поставить значок "О" (нолик) в любой открытый (т. е. еще
не занятый крестиками или ноликами) квадрат, тем самым завершая первый ход
первого игрока.

Что такое тестирование программного обеспечения?

17

4. Затем игроки должны по очереди размещать крестики и нолики (первый игрок, а
затем соответственно второй игрок) в свободные квадраты. Игра идет либо до
тех пор, пока не останется незаполненных квадратов, и при этом ни в одном ряду, колонке или диагонали не окажутся проставленые значки одного типа
(в этом случае объявляется ничья); либо до тех пор, пока целый ряд, колонка
или диагональ не окажутся заполнеными значками одного типа, и в этом случае
выставлявший значки данного типа (крестик для первого игрока, нолик для второго) оказывается победителем, а его соперник проигравшим.
Это довольно понятные правила игры в крестики-нолики. Теперь давайте рассмотрим случай, когда первый игрок, который должен поставить значок "Х", начинает
игру с "О". Это дефект, потому что это нарушает требование 2. Даже если продолжить игру (скажем, второй игрок поставит "Х"), все равно это дефект, потому что
это нарушает требование.
Теперь давайте представим, что после бета-тестирования пользователь заявляет,
будто игра нечестная, потому что она вынуждает игрока использовать "Х", а ему
этот значок не нравится. Пользователь предлагает заменить "Х" на "W", потому что
последняя является гораздо более красивой буквой. Это дефект или улучшение?
Это улучшение, потому что система соответствует всем требованиям и работает
нормально. То, что пользователю не нравится значок, не является дефектом!
И очень важно внести эти изменения — возможно, даже гораздо более важно, чем
исправлять существующие дефекты. Улучшения не являются плохими, бесполезными или каким-то видом жалоб, они просто предполагают модификацию существующих требований системы.
Другим примером дефекта стало бы то, если бы игровое поле исчезло после того,
как игрок поставил значок в центральный квадрат. Нет никаких особенных требований по этому поводу, но существуют изменяющиеся "неявные требования"
к программам, такие как запрет на аварийное завершение (им нельзя вылетать) и
зависание, они должны выводить на экране актуальное изображение, реагировать
на воздействие и т. д. Эти неявные требования могут изменяться в зависимости от
вида системы: например, видеоигра должна реагировать на воздействие в течение
99% всего времени, в то время как для программы прогноза погоды (в которой данные обновляются каждые 30 минут) "реагирование" заключается в том, чтобы просто вывести необходимую информацию.
Могут возникнуть разногласия по поводу того, является ли текущая проблема дефектом или улучшением. Основная причина этих разногласий — как раз эти неявные требования. Если персонаж видеоигры реагирует спустя три секунды после
нажатия клавиши, можно сказать, что это довольно долго, даже если нет отдельных
требований по производительности. Игроку не очень понравится каждый раз ждать
по три секунды. Но какое ожидание после нажатия клавиши является приемлемым?
Две секунды? Одна? Сто миллисекунд? Подобным образом можно задаться вопросом: допустимо ли для программы зависать и терять данные, если в системе
закончилась память? Для единственного запущенного приложения в вашем телефоне — возможно. Это допустимо принять как достаточно редкое событие, причи-

18

Глава 2

няющее небольшой урон обычному пользователю, поэтому добавление обработчика подобной ситуации станет улучшением. Для компьютера-мейнфрейма, на котором запущено ПО, работающее с банковскими переводами, это определенно окажется дефектом. Предотвращение потери данных, даже если это не указано явно
в требованиях, является крайне важным для этой области. Понимание тестируемой
системы и области ее применения позволяет вам применять интуитивное тестирование ("seat of your pants" testing), т. е. тестирование поведения без формального
описания, но основанное на вашем знании системы и области ее работы.
В некоторых случаях различие между дефектом и улучшением может оказаться
очень значительным. Если ваша компания разрабатывает авиационный софт для
нового истребителя, то очень вероятно, что у вас в скрупулезно рассматривается,
что именно является улучшением, а что — дефектом. В таком случае имеются подготовленные требования, принимающие решения арбитры и люди, чьей работой
является проектирование и разъяснение требований. Если компания подписала
контракт для разработки программы и тщательно придерживается каждой буквы
этого контракта, то ее сотрудники будут на всякое обращение заказчика отвечать,
что это не дефект, а нечто не покрытое требованиями, а значит, улучшение.
В других случаях граница между дефектами и улучшениями оказывается размытой.
Давайте предположим, что вы работаете на некий стартап, в котором недостаточно
средств на разработку программного обеспечения, а единственным реальным требованием выступает негласное правило "делайте то, что хочет клиент, или мы банкроты". В этом случае, если клиент хочет что-то, значит, над этим нужно работать
без каких-либо вопросов.
Хотя решение, над каким дефектом или улучшением следует работать, почти всегда находится в сфере управления проектом, а не обеспечения качества, взаимодействие с командой QA часто будет полезным. Тестирование ПО всегда позволяет
определить влияние найденных дефектов, а также потенциальные риски исправления дефектов, внесения улучшений или же просто найти обходные пути. Обладание этим знанием поможет менеджерам проекта принимать осознанные решения
о направлении развития продукта.

2.4. Пример тестирования в реальной жизни
Представим, что мы тестируем новую программу "Уменьшатель", которая берет
строку и делает ее буквы строчными. Заказчик не предоставил никакого дополнительного описания задачи, потому что она кажется очевидной — входной текст
может быть в нижнем регистре, а может и не быть, данные на выходе должны быть
той же строкой, но все прописные буквы должны стать строчными. Метод, решающий эту задачу в программе, имеет следующую сигнатуру:
public String lowerify(String S)

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

Что такое тестирование программного обеспечения?

19

для того, чтобы создать тест-план? Другими словами, какие требования вы бы постарались вытянуть из заказчика?
1. Какая кодировка символов будет использоваться — UTF-9, ASCII, EBCDIC или
что-то другое?
2. Какая максимальная длина обрабатываемых данных предполагается? То, что хорошо работает на нескольких словах, может начать вести себя по-другому, если
на вход подать несколько терабайт текста.
3. Что должно происходить, если входной текст написан на языке, отличном от
английского? И что делать, если в этом языке нет понятия прописных и строчных букв?
4. Что должна делать программа, если во время ее работы пользователь нажмет
комбинацию клавиш + или выполнит любую другую команду остановки программы?
5. Должна ли эта программа уметь читать данные из сети? Если да, то что нужно
делать в случае проблем с сетью — попытаться прочитать данные снова, прекратить работу, показать ошибку или что-то еще?
Уверенность в том, что у вас есть правильные ответы на эти вопросы, является частью валидации программы. Вы хотите тестировать не то, что пользователь хочет
получить от программы.
Вы разобрались, что хочет заказчик, но тем не менее еще остается над чем поработать. Вам нужно убедиться, что программа работает в нормальных условиях и обрабатывает разнообразные входные данные. Вот несколько вариантов входных
данных, позволяющих протестировать работу программы в различных случаях:
1. Строка со всеми прописными буквами, например "ABCDEFG".
2. Строка, в которой все буквы уже переведены в нижний регистр, например
"lmnop".
3. Строка с небуквенными символами, например "78&^%0()[]".
4. Строка, в которой смешаны буквы в верхнем и нижнем регистрах, например
"VwXyZ".
5. Строка со спецсимволами, такими как возврат каретки и нулевой символ, т. е.
\r\n\o.
6. Пустая строка.
7. Очень длинная строка — скажем, текст из большой книги, оцифрованной
в рамках "Проекта „Гутенберг“".
8. Исполняемый код.
9. Бинарные данные.
10. Строка, внутри которой встречаются маркеры конца файла (EOF).
Можете ли вы придумать какие-либо другие возможные входные данные, которые
способны вызвать ошибку или неправильный результат? Внешние факторы также
могут рассматриваться.

20

Глава 2

Что произойдет, если...
1. У системы закончится память во время обработки текста?
2. Процессор обрабатывает множество различных процессов, и система из-за этого
не отвечает на запросы?
3. Сетевое соединение оборвано в самый разгар обработки данных?
Важно отметить, что практически невозможно исчерпывающе протестировать
все комбинации входных данных. Даже если бы мы собирались протестировать
входные данные, ограниченные 10 символами из цифр и букв, и проигнорировать
все внешние факторы, тогда бы у вас получилось более трех квадриллионов тесткейсов. Так как строки могут быть произвольной длины (их ограничивает объем
памяти компьютера), и существует множество внешних факторов, которые можно
было бы учесть, выполнение всеобъемлющего тест-плана для этой функции заняло
бы миллиарды лет! Даже из этого простого примера легко понять, что тестирование
может быть очень сложным процессом, полным неопределенности и трудных решений о том, на чем следует сфокусироваться. Тестировщику предстоит не только
решать эти неопределенности, но и устанавливать, сколько усилий и времени следует тратить на них. Чем больше усилий вы вложите в какую-то часть тестирования, тем меньше времени останется на другое. Помните это, разрабатывая тестовую
стратегию, — время, за которое вы должны выполнить проект, может изменяться,
но оно всегда конечное, и всегда существуют различные приоритеты, которыми
вам придется жонглировать, чтобы обеспечить качество ПО.
Помните, что причиной проведения тестирования ПО являются оценка и, если возможно, уменьшение риска для заинтересованных лиц. Понимание возможных рисков само по себе может уменьшить риск. В конце концов, непротестированное программное обеспечение, которое никогда не запускалось, может быть идеальным
(теоретически, по крайней мере) или не работать совсем. Тестирование помогает
рассчитать, где между этими двумя экстремумами на самом деле находится программное обеспечение. Это поможет нам понять, являются ли проблемы с ПО тривиальными, или же из-за них следует отложить выпуск программы, потому что
бóльшая часть функционала не работает. Помогая определить уровень риска, тестировщики позволяют другим участвующим заинтересованным лицам принять соответствующие решения.

ГЛАВА 3

Зачем тестировать программы?
Теперь, когда мы знаем, что такое тестирование программного обеспечения, имеет
смысл спросить: почему кто-то может захотеть заниматься этим? В конце концов,
вы определенно добавляете дополнительную работу, т. к. придется писать тесты.
Также вам нужно будет убедиться, что созданный вами код может быть протестирован, и, возможно, создать тестовые фреймворки для своей системы. В зависимости от того, как вы тестируете, не исключено, что вам потребуется изучить другие
фреймворки или даже языки программирования.

3.1. Тестировать или не тестировать
Давайте представим, что вы стали главой компании Rent-A-Cat, Inc. Подающий надежды молодой менеджер проекта подбегает к вам в коридоре, с его прекрасно
уложенных волос капает пот, а сам он вцепился в распечатку из Excel.
"Мэм (или сэр)! — кричит менеджер проекта. — Я открыл способ снизить расходы
нашего проекта на десятки тысяч долларов! Всё, что необходимо, — это убрать
связанные с тестированием ресурсы из команды. У меня отличные разработчики
программ, и они никогда не сделают ошибку. Таким образом, мы, в конце концов,
сможем купить ту самую позолоченную раковину для туалета руководства!"
Здесь у вас есть два варианта на выбор:
1. Неистово захохотать и представить ощущение дистиллированной воды, текущей
на ваши наманикюренные руки в этой единственно подходящей для вас царской
раковине.
2. Объяснить менеджеру проекта причины тестирования и доводы, и почему важно
тестировать ПО перед его выпуском.
Поскольку вы читаете книгу по тестированию ПО, я предполагаю, что вы выберите
второй вариант. Хотя, на первый взгляд, вы могли подумать, что имело бы смысл
не тестировать вашу программу. С организационной точки зрения, фирма существует для того, чтобы принести доход своим владельцам. Если вы можете снизить
стоимость разработки программы путем исключения части процесса разработки,
тогда вы заставите основательно задуматься владельцев фирмы над тем, имеет ли
смысл содержать команду разработчиков.

22

Глава 3

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

3.2. Ни один из разработчиков не совершенен
Поднимите руку, если вы когда-либо писали неправильный программный код. Если
ваша рука поднята, тогда вы уже знаете, зачем нужно тестировать программы.
(Если ваша рука опущена, тогда я могу предположить, что вы никогда ранее не
программировали.) Неплохо помнить, что разработка программного обеспечения
является одним из самых интеллектуально сложных процессов, которым занимаются люди, и при этом максимально задействуются способности человеческого разума. И если так рассуждать, ошибка на единицу при получении индекса может показаться не такой уж серьезной.
Согласно данным Национального института стандартов и технологии (National
Institute of Standards and Technology), ошибки в ПО стоили американской экономике примерно 60 млрд долларов в 2002 году1. Это примерно 0,6% внутреннего валового продукта страны. И так как пишется всё больше и больше программ, а наша
повседневная жизнь всё больше и больше оказывается завязаной на них, то эта
цифра наверняка значительно выросла к сегодняшнему дню. Даже если рассматривать низкое значение 2002 года, можно понять, что дефекты в программах вызвали
проблемы, стоимость которых оказалась примерно равна трети американского
сельскохозяйственного производства.

3.3. Обнаружить дефекты раньше, чем позже
«Золотое правило» тестирования гласит, что вы должны обнаружить дефекты настолько раньше, насколько возможно. Если вы найдете проблему в программе
сравнительно рано, в большинстве случаев разработчику достаточно будет внести
простое исправление, и никто вне команды не узнает, что программа падала, когда
вводились цифры вместо имени. Если с подобным дефектом пользователь столк-

1

The Economic Impact of Inadequate Infrastructure for Software Testing, National Institute of Standards and
Technology, 2002. См. http://www.nist.gov/director/planning/upload/report02-3.pdf.

Зачем тестировать программы?

23

нется во время эксплуатации программы, последствия могут оказаться куда более
плачевными — компания потеряет деньги, т. к. приложение будет неработоспособным, а потребители разочароваными. Кроме того, заниматься исправлениями на
этом этапе окажется гораздо сложнее и дороже. Программисты в состоянии стресса
попытаются "заклеить скотчем", т. е. решить проблему как можно быстрее, и не
станут заниматься надежными исправлениями. Подобно заклеиванию протекших
труб скотчем, это решение окажется худшим из возможных.
Тестирование программ позволяет вам найти ошибки еще до того, как пользователи
увидели их, а ваша команда сможет эффективнее с ними справиться, уменьшив
в конечном итоге влияние этих ошибок.

3.4. Стабильность
Разработка программного обеспечения является сложным процессом. Иногда в некоторых компаниях она может даже превратиться в хаос. Команда тестировщиков
может облегчить ситуацию, обеспечивая стабильность. Подстраховка разработчиков в том, что они продолжают работу над содержащей меньше дефектов программой, позволит этой программе стать более устойчивой. Дополнительный функционал маловероятно может быть построен на основе ненадежного фундамента.
Команда обеспечения качества может также следить за существующими дефектами
и осуществлять расстановку приоритетов. Разработчикам и менеджерам не следует
бросать всё и заниматься только что обнаруженными дефектами или, наоборот,
игнорировать новые проблемы и акцентироваться на старых — команда тестировщиков сможет определить, чем следует заниматься в текущий момент. Это делает
процесс разработки более выверенным и равномерно идущим.

3.5. Защита пользователя
Программисты, менеджеры и все, кто работает над созданием программ, очень часто имеют свои причины для работы над проектом помимо того, что им платят за
эту работу. Программисты могут хотеть изучить другой язык программирования;
дизайнерам будет интересно попробовать создать принципиально новый пользовательский интерфейс; менеджеры проекта могут попробовать организовать свою
работу так, чтобы успевать всё в срок. История разработки программного обеспечения просто замусорена проектами, которые были технически интересны или
выпущены в срок, но при этом не отвечали требованиям пользователей.
У QA-инженеров есть особая роль — они действуют как представители потребителей и пользователей программы. Эта роль позволяет убедиться, что потребители
получат программу высокого качества, которая им нужна. Фактически в некоторых
организациях у тестировщиков есть полномочия остановить релиз или переопределить ресурсы для того, чтобы пользователь в итоге получил необходимую ему программу, которая разработана нужным образом. Эти полномочия зависят от области,
в которой работает компания; в компаниях, создающих критически важные или

24

Глава 3

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

3.6. Независимый взгляд на всю систему
Разработчики сфокусированы на малой части системы, с которой они близко знакомы. Редко кто из разработчиков понимает функционирование всей разрабатываемой системы. У тестировщиков может не быть глубокого понимания каждой
составляющей программы, но они, как правило, имеют широкое видение всей системы. Они тестируют различные возможности программы, используют новую
функциональность и устанавливают программу в различных системах.
Такой охват всей системы оказывается ценной противоположностью глубокому
погружению в какую-то одну из составляющих программы. Понимание, как совместно работают различные подсистемы и как реализована функциональность с точки
зрения пользователя, позволяет сотрудникам QA предоставить информацию о статусе системы как системы. Это также делает тестировщиков ценным ресурсом при
добавлении новой функциональности или модификации существующей, т. к. они
будут понимать, как это может повлиять на систему в целом.
В то время как другие заинтересованные лица будут напрямую заинтересованы
в разработке программы, роль тестировщика будет заключаться в обеспечении независимого взгляда на программу. Рассматривая систему изолированно и без влияния (вольного или невольного) разработчиков и менеджеров, тестировщики ПО
могут предоставить более реалистичный и объективный статус системы.

3.7. Обеспечивая качество
Хотя тестирование ПО может предоставить вам множество ценных преимуществ,
оно не является единственным способом улучшить качество вашей программы.
В одной из самых авторитетных книг по написанию программ "Совершенный код"
Стива Макконнела (Steve McConnell "Code Complete") приведена оценка нахождения дефектов разработчиками, использующими различные техники. Анализ кода
(code review), формальные инспекции, моделирование программного обеспечения
способствуют улучшению качества программы. Парное программирование, когда
два человека работают одновременно за одним компьютером, также показало значительное улучшение качества программы. Так как человеку довольно легко пропустить свои ошибки, другой человек, изучающий код или текст независимо,
довольно часто замечает, что что-то не так. Я знаю, о чем говорю, — я пропустил
множество нелепых опечаток в этой книге, которые были обнаружены сразу же,
как я отправил книгу на редактирование1.
1

См. https://github.com/laboon/ebook/pull/15/.

Зачем тестировать программы?

25

Хотя качество означает не только "уменьшение количества дефектов", это определенно является важной составляющей. Выявляя дефекты, тестировщики улучшают
качество программы. Пользователи программы получают лучший продукт.
Выделение соответствующего времени на тестирование, программирование и прочие составляющие цикла разработки программного обеспечения также поможет
повысить качество. Очень немногие части программы написаны безупречно, а созданная в условиях недостатка времени программа окажется неприемлемого качества. Предоставление адекватного времени и ресурсов инженерам для разработки
программы в целом повысит уровень качества.
Выбор языка программирования, фреймворка и дизайн программы также могут
оказать большое влияние на качество программы. Хотя у каждого языка есть свои
сторонники, определенно существуют причины, по которым большинство вебприложений не пишется на ассемблере, а встраиваемое ПО не создается на Ruby.
Разные языки, библиотеки и другие средства разработки имеют различные преимущества, и использование тех, что подходят для вашей создаваемой системы,
принесет свои дивиденды в терминах качества.
Хотя эта книга фокусируется на тестировании, следует признать, что оно выступает
только одной из составляющих качественного ПО. Качество в современном проекте по разработке ПО является обязанностью всех участников команды, не только
тестировщиков.

3.8. Риск
Сущность тестирования сводится к минимизации рисков для всех вовлеченных:
потребителей, пользователей, разработчиков и т. д. Независимое тестирование ПО
позволит провести объективный анализ качества системы. Оно снижает риск путем
предоставления информации о статусе системы как на высоком уровне (т. е. "система готова к релизу"), так и на низком (т. е. "если имя пользователя содержит символ !, то система сломается"). Разработка ПО является сложным и рискованным
процессом. И если руководитель компании хочет убедиться, что риски сведены
к минимуму, необходимо, чтобы тестировщики ПО являлись частью команды.

ГЛАВА 4

Основы тестирования
Перед тем как мы погрузимся в написание тестов, нам надлежит убедиться, что мы
с вами одинаково понимаем теорию и терминологию тестирования. В этой главе
мы расширим свой словарный запас и получим теоретическую основу для обсуждения тестирования.

4.1. Классы эквивалентности и поведение
Представьте, что вам необходимо протестировать новый дисплей для датчика давления автомобильных колес. Давление считывается с внешнего датчика, и гарантируется, что значение давления будет передано на наш 32-битный дисплей. Если
давление больше 35 фунтов на квадратный дюйм (pounds per square inch, PSI), должен загореться сигнал "ИЗБЫТОЧНОЕ ДАВЛЕНИЕ", а все остальные сигналы
должны быть отключены. Если давление находится в пределах от 0 до 20 PSI, должен загореться сигнал "НЕДОСТАТОЧНОЕ ДАВЛЕНИЕ", и все остальные сигналы должны быть отключены. Если значение давления оказывается отрицательным,
должен загореться сигнал "ОШИБКА", и все остальные сигналы должны быть отключены.
Этот тест должен быть довольно простым. Имеется только одно входное значение,
его тип известен, и выходные значения тоже известны. Мы исключаем экзогенные
факторы, хотя тестировщику "железа" будет интересно узнать, что случится, если,
скажем, провод между датчиком и дисплеем оборвется, или произойдет скачок напряжения, или... в общем, используйте свое воображение.
С чего надо начать подобное тестирование? Вам нужно подготовить некоторые
входные значения и ожидаемые выходные (например, "отправить 15 PSI → увидеть, что горит сигнал „НЕДОСТАТОЧНОЕ ДАВЛЕНИЕ“ и все остальные сигналы
выключены"). Вы можете исполнить тест и посмотреть, совпадает ли то, что происходит, с тем, что вы ожидали увидеть. Это суть тестирования — сверка ожидаемого поведения с наблюдаемым поведением, т. е. обеспечение того, что программа
делает именно то, что вы ожидаете от нее в определенных обстоятельствах. Можно
внести корректировки, дать советы и предостережения, но основа всего тестирования заключается в сравнении ожидаемого поведения с наблюдаемым.

Основы тестирования

27

Ваш менеджер хотел бы протестировать эту систему как можно быстрее и поручает
вам создать четыре теста. Вооруженные знанием, что вам нужно сравнить ожидаемое поведение с наблюдаемым, вы решаете отправить значения –1, –111, –900 и –5,
чтобы увидеть сигнал "ОШИБКА" в каждом случае, и при этом остальные сигналы
не должны загораться. Волнуясь от того, что вы написали свои первые четыре теста, вы показываете их менеджеру, который хмурится и говорит: "Ты тестируешь
только один класс эквивалентности!"
Класс эквивалентности (или эквивалентное разбиение) является набором входных
данных, которые соответствуют одному выходному значению. Вы можете представить их как различные "группы" входных значений, которые делают что-то схожее.
Это дает возможность тестировщикам создавать тесты, которые покрывают все составляющие функциональности и позволяют избежать избыточного тестирования
только одной части (как в вышеприведенном примере, где класс эквивалентности
"ОШИБКА" был протестирован четыре раза, в то время как другие ни одного).
Какие другие классы эквивалентности имеются в данном случае? Для того чтобы
найти ответ, представьте все возможные варианты, которые вы можете получить на
выходе:
1. Сигнал "ОШИБКА" загорается для PSI, равного –1 или меньше.
2. Сигнал "НЕДОСТАТОЧНОЕ ДАВЛЕНИЕ" загорается для PSI между 0 и 20
включительно.
3. Сигналы не загораются для PSI между 21 и 35 включительно — нормальные
условия работы.
4. Сигнал "ИЗБЫТОЧНОЕ ДАВЛЕНИЕ" загорается для PSI от 36 и выше.
Математически вы можете связать группу входных значений и ожидаемое состояние на выходе:
1. [MININT, MININT + 1, ..., –2, –1] → только сигнал "ОШИБКА".
2. [0, 1, ..., 19, 20] → только сигнал "НЕДОСТАТОЧНОЕ ДАВЛЕНИЕ".
3. [21, 22, ..., 34, 35] → нет сигналов.
4. [36, 37, ..., MAXINT – 1, MAXINT] → только сигнал "ИЗБЫТОЧНОЕ
ДАВЛЕНИЕ".
(Здесь MAXINT и MININT являются максимальным и минимальным значениями
32-битного целого числа соответственно.)
Мы только что разбили наши эквивалентные классы. Разбиение является определением наших эквивалентных классов и гарантированием того, что они не перекрывают друг друга, но при этом покрывают все входные значения. Другими словами, они должны поддерживать строгое разбиение. Например, из-за плохих или
неправильно понятых требований мы сгенерировали следующее разбиение эквивалентных классов:
1. [–2, –1, 0, 1, 2] → только сигнал "ОШИБКА".
2. [3, 4, ..., 21, 22] → только сигнал "НЕДОСТАТОЧНОЕ ДАВЛЕНИЕ".

28

Глава 4

3. [20, 21, ..., 34, 35] → нет сигналов.
4. [36, 37, ..., 49, 50] → только сигнал "ИЗБЫТОЧНОЕ ДАВЛЕНИЕ".
Здесь есть две проблемы. Первая заключается в том, что все значения меньше –2 и
больше 50 не привязаны к классам эквивалентности. Каким должно быть ожидаемое поведение, если датчик отправит значение 51? Это также рассматривается как
ошибка? Или это "ИЗБЫТОЧНОЕ ДАВЛЕНИЕ"? В данном случае это не определено. Очень часто неопределенное поведение встречается при тестировании довольно сложных программных комплексов, но тестировщик ПО должен помогать
в нахождении пробелов в покрытии и узнавать, что должно происходить (или, по
крайней мере, происходит) в таких ситуациях.
Вторая и гораздо более неприятная проблема заключается в противоречивости
принадлежности значений 20, 21 и 22. Они одновременно принадлежат к классам
эквивалентности "НЕДОСТАТОЧНОЕ ДАВЛЕНИЕ" и "нет сигналов". Какое ожидаемое поведение для входного значения 21? В зависимости от того, какой эквивалентный класс вы рассматриваете, это может быть отсутствие сигналов или сигнал
"НЕДОСТАТОЧНОЕ ДАВЛЕНИЕ". Это нарушение строгого разбиения, и вы легко
можете увидеть, насколько проблематичным оно может быть.
Важно отметить, что эквивалентные классы не должны состоять из случаев, которые дают одинаковое выходное значение! Например, представим, что вы тестируете интернет-магазин. При стоимости заказа 100 долларов и менее предоставляется
скидка 10%, а при заказе на 100,01 доллара и более — скидка 20%. И хотя здесь
есть широкий диапазон выходных значений, поведение на выходе будет одинаковым для всех значений от 100 долларов и меньше и для всех значений от
100,01 доллара и больше. Они образуют два эквивалентных класса, и не будет отдельных классов для каждого индивидуального выходного значения (т. е. $10,00 →
$9,00; $10,10 → $9,01 и т. д.).
Теперь, когда наши эквивалентные классы определены, можно написать тесты, которые покрывают всю функциональность дисплея. Мы можем отправить значение –2, чтобы протестировать класс эквивалентности "ОШИБКА", значение 10,
чтобы протестировать класс эквивалентности "НЕДОСТАТОЧНОЕ ДАВЛЕНИЕ",
значение 30 для тестирования класса эквивалентности "НЕТ СИГНАЛОВ" и значение 45, чтобы протестировать класс эквивалентности "ИЗБЫТОЧНОЕ ДАВЛЕНИЕ". Конечно, эти значения были выбраны, скорее, произвольно. В следующей
главе мы рассмотрим, как выбирать определенные значения, чтобы увеличить шансы нахождения дефектов.

4.2. Внутренние и граничные значения
В тестировании существует аксиома, что дефекты, скорее всего, могут быть найдены на границах двух классов эквивалентности. Эти значения — "последнее" одного
класса эквивалентности и "первое" следующего класса эквивалентности — называются граничными значениями (boundary values). Значения, которые не являются граничными, называют внутренними значениями (interior values). Например,

Основы тестирования

29

рассмотрим очень простую математическую функцию, в которой вычисляется
абсолютная величина целого значения. У нее есть два класса эквивалентности:
1. [MININT, MININT + 1, ..., –2, –1] → для входного значения x на выходе получим –(x).
2. [0, 1, ..., MAXINT – 1, MAXINT] → для входного значения x на выходе получим x.
Граничные значения здесь — это –1 и 0; они являются разделительной линией между
двумя классами эквивалентности. Любое другое значение (например, 7, 62, –190)
будет внутренним, окажется "в середине" класса эквивалентности.
Теперь, когда мы понимаем, что такое граничные и внутренние значения, можно
задаться вопросом: почему случай с использованием граничных значений более
вероятно окажется дефектным? Причина в том, что более вероятно, что в коде окажется ошибка на граничном значении, потому что классы эквивалентности оказываются близки. Давайте рассмотрим пример с функцией, вычисляющей абсолютную величину:
public static int absoluteValue (int x) {
if (x > 1) {
return x;
}
else {
return –x;
}
}

Вы видите ошибку в коде? Здесь простая ошибка на единицу в первой строке метода. Поскольку проверяется, что аргумент больше единицы, подача на вход 1 вернет
–1. Так как граничные значения очень часто явно упоминаются в коде, это еще одна
причина, по которой они могут вызвать ошибку или попасть не в "тот" класс эквивалентности. Давайте перепишем метод правильно, с учетом найденной ошибки:
public static int absoluteValue (int x) {
if (x >= 1) {
return x;
} else {
return –x;
}
}

Гораздо лучше. Однако представьте, как трудно будет переписать этот метод, чтобы он выдавал ошибку, только когда вы передаете 57 — не 56 и 58, а именно 57.
Такое возможно, конечно, но маловероятно. Так как очень редко имеется возможность исчерпывающе протестировать все входные значения для программы (или
даже для одиночной функции), имеет смысл сфокусироваться на значениях, которые помогут вскрыть дефекты.
Вернемся к нашему дисплею, отображающему информацию о давлении, — наш
тест-менеджер говорит, что у нас есть время протестировать больше значений. Мы

30

Глава 4

хотим удостовериться, что как минимум мы тестируем все граничные значения
и достаточный набор внутренних значений. Сперва мы рассчитаем все граничные
значения, а затем создадим тест-план, в котором будут все граничные значения
и некоторые из внутренних.
Граничные значения:
1. –1, 0 (граница между "ОШИБКА" и "НЕДОСТАТОЧНОЕ ДАВЛЕНИЕ").
2. 20, 21 (граница между "НЕДОСТАТОЧНОЕ ДАВЛЕНИЕ" и "НОРМАЛЬНО").
3. 35, 36 (граница между "НОРМАЛЬНО" и "ИЗБЫТОЧНОЕ ДАВЛЕНИЕ").
Значения, которые необходимо протестировать:
1. Внутренние значения, "ОШИБКА": –3, –100.
2. Граничные значения, "ОШИБКА"/"НЕДОСТАТОЧНОЕ ДАВЛЕНИЕ": –1, 0.
3. Внутренние значения, "НЕДОСТАТОЧНОЕ ДАВЛЕНИЕ": 5, 11.
4. Граничные
20, 21.

значения,

"НЕДОСТАТОЧНОЕ

ДАВЛЕНИЕ"/"НОРМАЛЬНО":

5. Внутренние значения, "НОРМАЛЬНО": 25, 31.
6. Граничные значения, "НОРМАЛЬНО"/"ИЗБЫТОЧНОЕ ДАВЛЕНИЕ": 35, 36.
7. Внутренние значения, "ИЗБЫТОЧНОЕ ДАВЛЕНИЕ": 40, 95.
Можно также рассмотреть неявные граничные значения. В отличие от явных
граничных значений, которые являются результатом требований (подобно тем,
что рассчитаны выше), неявные значения определяются тестируемой системой или
средой, в которой работает система. Например, MAXINT и MININT — неявные
граничные значения; добавление единицы к MAXINT вызовет превышение максимально допустимого целого значения, а присвоение переменной значения MININT
и последующее его уменьшение на единицу приведут к тому, что переменная примет значение MAXINT. В каждом из этих случаев класс эквивалентности изменится.
Неявные граничные условия могут зависеть от условий исполнения. Предположим,
что у нас есть система с 2 Гбайт памяти, и мы запускаем в памяти обработку базы
данных. Классы эквивалентности для тестирования функции, которая вычисляет
количество добавленных строк, могут быть следующими:
1. Отрицательное число строк → ошибочное условие.
2. Ноль строк, или таблица не существует → возвращается NULL.
3. Одна строка или более → возвращается количество добавленных строк.
Существует неявная граница между тем количеством строк, которые можно добавить в память, и тем, которые нельзя. Тот, кто составлял требования, мог не подумать об этом, но вы, как тестировщик, должны держать в уме неявные граничные
значения.

Основы тестирования

31

4.3. Базовые случаи, граничные случаи,
угловые случаи
Продолжим наше изучение сенсорного датчика давления. Пройдя через различные
тестовые ситуации (тест-кейсы), мы можем понять, что они различаются по своей
распространенности. Бóльшую часть времени давление либо будет нормальным,
либо немного меньше, либо немного больше. Каждый из таких случаев является
базовым (base case) — система работает с ожидаемыми параметрами в обычном
режиме.
Ситуация, когда входные значения лежат за границами нормальных рабочих параметров или приближаются к тем пределам, которые еще может обработать система,
называется граничным случаем (edge case). Граничным случаем является дыра
в шине и падение давления до нуля. Другим случаем станет то, что кто-то забыл
про подключенный насос, и из-за этого давление подскочило до 200 PSI, что является максимально допустимым значением для шины.
Угловые случаи (corner case, иногда называемые патологическими случаями)
относятся к ситуациям, когда сразу несколько составляющих работают неправильно в одно и то же время, или значение, грубо говоря, совершенно не попадает
в диапазон ожидаемых значений. В качестве примера можно привести датчик значения шины, получающий значение 2 млрд PSI, что значительно выше давления
внутри ядра Земли. В качестве другого примера можно привести шину, лопнувшую
в тот момент, когда датчик ломается и пытается отправить сообщение об ошибке.
Хотя я использовал простую функцию с относительно хорошо заданными входными и выходными значениями, базовые случаи, граничные случаи и угловые случаи
могут быть определены и изучены через другие виды операций. Рассмотрим интернет-магазин. Для тестирования корзины покупателя возможны следующие базовые
случаи:
1. Добавить товар в пустую корзину.
2. Добавить товар в корзину, в которой уже присутствует товар.
3. Удалить товар из корзины, в которой уже присутствует товар.
Это всё счастливые пути — данные являются валидными, обычными, а проблемы
отсутствуют. Нет ошибок и не возникают исключения, скорее всего, их создаст
пользователь, система работает нормально и т. д. Теперь давайте рассмотрим граничные случаи:
1. Пользователь пытается добавить 1000 единиц товара в корзину одновременно.
2. Пользователь пытается нажать кнопку Удалить в корзине, которой нет товаров.
3. Пользователь открывает и закрывает корзину множество раз, ничего при этом
больше не делая.
4. Пользователь пытается добавить в корзину товар, которого нет на складе.
Все эти случаи могут произойти, но они не "нормальные". Они могут потребовать
специальных обработчиков ошибок (таких, как попытка удалить товары из пустой

32

Глава 4

корзины или добавить товар, которого нет в продаже), иметь дело с большими числами (например, добавление 1000 предметов) или нагружать систему странным
способом (открывать и закрывать корзину снова и снова).
В итоге угловые случаи являются случаями, в которых возникают самые разрушительные проблемы или задействуются очевидно плохие данные. Несколько примеров:
1. Товар, который был в наличии на момент загрузки страницы, закончился на
складе перед тем, как пользователь нажал кнопку Добавить в корзину.
2. Система получает запрос добавить 1080 товаров (что примерно соответствует
количеству атомов во Вселенной) в корзину.
3. Память, в которой хранилось содержимое корзины, оказалась повреждена.
Угловые случаи часто включают в себя катастрофические сбои (потерю сетевого
соединения, поломку ключевой подсистемы), генерацию полностью неправильных
данных или множественные сбои, произошедшие одновременно.

4.4. Успешные и неуспешные случаи
При обсуждении тест-кейсов предполагается, что у теста может быть два варианта
завершения. Во-первых, это может быть успешный случай (также называемый
позитивным тест-кейсом), в котором результат соответствует ожидаемому при
определенных данных. В общем, тесты, следующие по счастливому пути, когда
пользователь делает то, что обычно должен делать, являются успешными случаями.
Неуспешными случаями (также называемые негативными тест-кейсами) являются случаи, когда мы ожидаем "падение" системы по каким-либо причинам, например при попытке осуществить запись на диск, доступный только для чтения,
вычислить квадратный корень из отрицательного числа (в системах, которые не
работают с комплексными числами) или при добавлении неправильного имени
пользователя в систему. В неуспешных случаях вместо того, чтобы прийти к корректному результату, система выполнит... что-то другое. Это "что-то другое" зависит от теста и от того, какая функциональность тестируется. Это может быть ошибка кода, возврат значения по умолчанию, исключение, закрытие системы или просто вывод информации об ошибке в лог-файл или в консоль.

4.5. Тестирование
черного, белого и серого ящиков
Существуют различные способы тестирования системы, каждый со своими достоинствами и недостатками. Сейчас мы познакомимся с тремя различными видами
тестирования, а более подробно рассмотрим эти три парадигмы в следующих главах.
Возможно, самым легким видом тестирования для понимания является тестирование черного ящика. В этом случае у тестировщика нет знаний о том, как работает

Основы тестирования

33

система изнутри, и взаимодействовать с ней он может как обычный пользователь.
Другими словами, тестировщик не знает, какая используется база данных, какие
существуют классы или даже на каком языке написана программа. Вместо этого
тестирование проводится так, как будто тестировщик является обычным пользователем программы.
Рассмотрим приложение для работы с электронной почтой. Взявший на тестирование это приложение тестировщик черного ящика будет проверять, может ли оно
принимать и отправлять почту, проверять грамотность слов в письме, сохранять
файлы и т. п. Этот тестировщик не станет проверять, что был вызван какой-то метод некоего класса, что объекты загружены в память или как осуществляются вызовы определенных функций. Если, например, тестировщик хочет убедиться, что
письма могут быть правильно отсортированы по именам отправителей, в соответствующем тесте черного ящика нужно нажать кнопку или выбрать в меню команду
Сортировать по именам отправителей. Тестировщик черного ящика может не
знать, что программа написана на Java или Haskell, использовалась ли сортировка
слиянием, быстрая сортировка или пузырьковая сортировка. Хотя тестировщика
будут беспокоить результаты, полученные благодаря выбранным методам. Тестировщик черного ящика фокусируется на том, работает ли тестируемая система, как
должна, с точки зрения пользователя, и нет ли в ней дефектов, с которыми может
столкнуться пользователь.
Особенности системы, такие как вид примененного алгоритма или тип выбранной
схемы использования памяти, могут предполагаться тестировщиком черного ящика, но он в первую очередь должен сфокусироваться на результатах работающей
системы. Например, тестировщик черного ящика может обратить внимание, что
система замедляется, когда начинает сортировать тысячи писем в почтовом ящике
пользователя. Тестировщик черного ящика может предположить, что для сортировки использовался алгоритм типа O(n2), и отметить это как дефект. Тем не менее
он не будет знать, какой алгоритм применялся или какие особенности программного кода вызвали замедление.
По знанию работы тестируемой системы тестирование белого ящика является
противоположностью тестированию черного ящика. В тестировании белого ящика
у тестировщика есть глубокое представление об исходном коде, и он напрямую
тестирует этот код. Тестировщик белого ящика может протестировать отдельные
функции кода, очень часто изучая особенности работы системы гораздо более детально, чем в тестах черного ящика.
Продолжая рассматривать пример с почтовой программой, обратим внимание, что
тесты белого ящика могут проверять функцию сортировки (EmailEntry[] emails),
подавая на вход различные значения и изучая то, что эта функция возвращает или
делает. Тестировщиков белого ящика будет беспокоить, что именно произойдет,
если были переданы массив нулевой длины или ссылка на нулевой элемент, в то
время как тестировщики черного ящика будут озабочены только тем, что случится,
если они попытаются отсортировать пустой список сообщений в самом приложении. Тестировщики белого ящика обращаются с кодом как с кодом — проверяют,

34

Глава 4

что возвращаемое значение функции корректное, объекты инициализированы правильно и т. д. — вместо того чтобы смотреть на систему с точки зрения пользователя.
Тестирование серого ящика, как подразумевает его название, является гибридом
тестирования белого ящика и тестирования черного ящика. Тестирование серого
ящика включает обращение тестировщика к системе в роли обычного пользователя
(как в тестировании черного ящика), но со знаниями программного кода и устройства системы (как в тестировании белого ящика). Обладая этими знаниями, тестировщик серого ящика может создавать более глубокие тесты черного ящика.
Давайте предположим, что наш тестировщик серого ящика собирается протестировать функцию сортировки в почтовом приложении. Изучая код, тестировщик
понимает, что система использует метод сортировки вставками. Известно, что
у сортировки вставками наихудшая производительность, когда необходимо отсортировать данные, уже отсортированные в обратном порядке. Таким образом, тестировщик серого ящика может добавить тест, который проверяет, что система способна отсортировать список электронных писем, которые расположены в обратном
порядке. Другой пример — тестировщик может обратить внимание, что в функции
поиска нет обработки пустых данных, и проверить, приведет ли простое нажатие
клавиши без ввода данных к разыменованию нулевого указателя
в программном коде, к поиску "" и отображению всех писем в результатах поиска,
или к другому непредсказуемому поведению.

4.6. Статическое и динамическое тестирование
Другим способом систематизации тестов является их группировка на статические
и динамические тесты. В динамических тестах тестируемая система работает и
код исполняется. Фактически все тесты, которые мы рассматривали ранее, были
динамическими. Даже если мы не видим сам код, компьютер работает, принимая
какие-то данные на входе, обрабатывает их и выдает выходные данные.
Статический тест, наоборот, не исполняет код. Скорее, он пытается протестировать
особенности системы без запуска самой системы. Примерами статического тестирования могут быть запуск линтера (который помечает "дурно пахнущий код" —
скажем, в котором к переменной обращаются еще до присваивания ей какогонибудь значения) или когда кто-то проверяет код вручную без его запуска.
На первый взгляд, преимущества статического тестирования могут быть неочевидны. В конце концов, какую дополнительную выгоду можно получить от тестирования программы без ее запуска? Вы как будто осознанно отстраняетесь от прямого
воздействия и наблюдаете всё со стороны. Однако, поскольку статический анализ
напрямую изучает код вместо результатов исполнения этого кода, он может помочь
найти проблемные участки самого кода.
В качестве примера давайте рассмотрим два метода, которые принимают на входе
строковую переменную toChirp и добавляют в конце ее строку "CHIRP!". Например,
передача значения foo вернет fooCHIRP! в обоих методах:

Основы тестирования

35

public String chirpify(String toChirp) {
return toChirp + "CHIRP!";
}
public String chirpify(String toChirp) {
char[] blub = toChirp.toCharArray();
char[] blub2 = char[blub.length + 6];
blub2[blub.length + 0] = (char) 0x43;
blub2[blub.length + 1] = (char) 0110;
blub2[blub.length + 2] = (char) 73;
blub2[blub.length + 3] = (char) (01231);
blub2[blub.length + 4] = (char) (40*2);
blub2[blub.length + 5] = "!";
String boxer99 = String.copyValueOf(data2);
return boxer99;
}

Оба этих метода будут возвращать один результат для определенного входного
значения. Их выходные данные могут быть изучены динамическими тестами и сверены с ожидаемыми значениями. Однако можно ли сказать, что второй метод лучше? Он довольно запутанный; сложно понять, что он делает; имена переменных
бессмысленные. Поиск всего того, что неправильно в коде, оставим в качестве
упражнения читателю. В случае с динамическими тестами может оказаться сложно
или невозможно определить разницу между этими двумя методами. Но, используя
такой метод статического тестирования, как обзор кода (code review), довольно
просто найти проблемы во втором методе.

ГЛАВА 5

Требования
Запомните, что валидация ПО заключается в гарантировании, что мы создаем правильное программное обеспечение; другими словами, убеждаемся, что создаем то,
что хотят пользователи и/или потребители. Для того чтобы провалидировать программу, мы должны знать, что она должна делать, по мнению пользователей, и эта
задача может оказаться гораздо более сложной, чем вы думаете. Пользователи часто не способны четко сформулировать, что именно они хотят. Они могут думать,
что знают точно свои желания, но когда они видят реализацию, то сразу же понимают, что это не то. Или у них может вообще не быть никакой идеи — они просто
хотят от вашей команды, чтобы вы создали "что-то вроде социальной сети, но для
покупок и продаж. Что именно? Что-то типа хот-догов. А для кошек можно?".
Одним из способов определить, какое ПО создавать, является определение требований к программному обеспечению. Требования являются формулировками,
определяющими, что именно эта часть программы должна делать в определенных
условиях. Так более или менее принято в зависимости от области работы и вида
создаваемого программного обеспечения. Например, при создании программы для
мониторинга ядерного реактора должны быть очень точные и хорошо проработанные требования. Если вы работаете для социально-медийного стартапа (надеюсь, не
того, который "продажа и покупка, типа того, например, хот-догов... для кошек",
потому что среди них конкуренция), тогда ваши "требования" могут быть нацарапаны вашим CEO на салфетке после нескольких бутылок вина и после нескольких
встреч с реальными потребителями.
Требования гарантируют, что разработчики знают, что создавать, а тестировщики
знают, что тестировать. Хотя требования очень важны, они не являются священными! Здравый смысл должен преобладать при изучении требований, и требования
могут быть изменены. Обратите внимание, что помимо требований существуют
другие способы определения, какое ПО создавать, например пользовательские истории (user stories).
В нашем примере с датчиком давления колес из главы по основам тестирования
у нас были сравнительно простые требования, хотя мы не оговаривали, чем именно
они являются:
1. Сигнал "ОШИБКА" включается для PSI, равного –1 или меньше.
2. Сигнал "НЕДОСТАТОЧНОЕ ДАВЛЕНИЕ" включается для PSI от 0 до 20.

Требования

37

3. Сигналы не включаются для PSI от 21 до 35 (нормальные рабочие условия).
4. Сигнал "ИЗБЫТОЧНОЕ ДАВЛЕНИЕ" включается для PSI от 36 и выше.
Эти "неформальные требования" показывают, что должно происходить с системой
при определенных входных значениях. Классический способ написания требований
заключается в том, чтобы сказать, что система "должна" сделать. При тестировании
вы можете мысленно перевести "должна" как "обязана". То есть если требование
говорит, что система должна сделать что-то, значит, система обязана сделать что-то
для того, чтобы вы как тестировщик могли сказать, что система соответствует требованию. Требования должны быть написаны точно, и следует избегать неоднозначности любой ценой. Давайте переведем эти неформальные требования в нечто,
больше соответствующее требованиям, которые пишутся в реальном мире.
 REQ-1. Если дисплейным датчиком получено значение давления, равное –1 или

меньше, тогда сигнал "ОШИБКА" должен быть включен, а все остальные сигналы должны быть отключены.
 REQ-2. Если дисплейным датчиком получено значение давления, лежащее

в пределах от 0 до 20 (включительно), тогда сигнал "НЕДОСТАТОЧНОЕ
ДАВЛЕНИЕ" должен быть включен, а все остальные сигналы должны быть отключены.
 REQ-3. Если дисплейным датчиком получено значение давления, лежащее

в пределах от 21 до 35 (включительно), тогда все сигналы должны быть отключены.
 REQ-4. Если дисплейным датчиком получено значение давления, равное 36 или

больше, тогда сигнал "ИЗБЫТОЧНОЕ ДАВЛЕНИЕ" должен быть включен, а
все остальные сигналы должны быть отключены.
Обратите внимание, насколько подробными эти требования стали по сравнению
с приведенными выше неформальными. Инжиниринг требований является настоящей инженерной дисциплиной, и может оказаться очень сложно описать большую
и/или сложную систему. Также обратите внимание, насколько более плотным и
бóльшим стал текст в попытке избежать неоднозначности. Определенно, на написание формальных требований уйдет гораздо больше времени, чем накалякать общий набросок программы на салфетке. Теперь и изменения вносить станет сложнее. Компромисс может быть найден, а может и нет, в зависимости от области работы и тестируемой системы. Гарантирование того, что авиационное программное
обеспечение при всех условиях правильно контролирует полет самолета, вероятно,
потребует тщательно проработанной спецификации требований (списка всех требований системы), в то время как для упомянутого выше медийно-социального
сайта подобное может не понадобиться. Жесткость может обеспечить очень хорошее определение системы, но ценой гибкости.
Кстати, если вы задумывались, почему адвокатам платят так много, то можно сказать, что описанное выше применимо к тому, чем они занимаются каждый день.
Можно представить, что законы — это набор требований, которые человек должен
соблюдать, чтобы быть законопослушным; представить, какие наказания применяются в случае, если человек нарушает закон, как создаются законы и т. д.:

38

Глава 5

1. Если нарушитель переходит оживленную дорогу не по пешеходному переходу,
и имеются приближающиеся машины, и по крайней мере одной машине необходимо затормозить, то нарушитель должен быть обвинен в нарушении "Пешеход
не уступил дорогу".
2. Если человек переходит оживленную дорогу, когда на светофоре пешеходного
перехода не горит зеленый свет и нет приближающихся машин, нарушитель
должен быть обвинен в нарушении "Пешеход не соблюдает сигналы светофора".
И в законе, и в тестировании возможно "спуститься в кроличью нору", стараясь
определить, что именно означает текст. Английский язык полон неоднозначностей.
Например, возьмем совершенно разумное требование, которое может быть прочитано различными способами, один из которых:
 UNCLEARREQ-1. Основная сигнализационная система должна звучать, если
обнаружен нарушитель с помощью визуального дисплейного модуля.
Должно ли это означать, что сигнализация должна зазвучать, если нарушитель
пользовался помощью визуального дисплейного модуля? Или сигнализация должна
звучать, если нарушитель обнаружен благодаря визуальному дисплейному модулю
системы? Старайтесь избегать такой неоднозначности при написании требований.
Хотя тестировщики редко пишут требования самостоятельно, их частенько просят
просмотреть их. Гарантирование того, что вы понимаете требование и это требование может быть протестировано, сохранит время и позволит позднее избежать
головной боли в процессе разработки.
При написании требований важно держать в голове, что требования обозначают,
что системе следует делать, но не как ей это следует делать. Другими словами, не
надо определять детали реализации, а лишь как система или подсистема взаимодействует c окружающим миром и воздействует на него. Это легче понять на примерах из новой межзвездной космической игры.
Хорошие требования:
 GOOD-REQ-1 — когда пользователь нажимает кнопку Скорость, текущая скорость космического корабля должна быть отображена на главном экране;
 GOOD-REQ-2 — система должна быть способна поддерживать скорость 0,8с
(80% скорости света) по крайней мере в течение трех дней (7200 часов) без потребности в техобслуживании;
 GOOD-REQ-3 — система должна сохранять последние 100 координат мест,
в которых брались образцы.
Плохие требования:
 BAD-REQ-1 — когда пользователь нажимает кнопку Скорость, система должна
обратиться к памяти по адресу 0x0894BC50 и отобразить ее значение на экране;
 BAD-REQ-2 — система должна моделировать реакцию "антивещество —
вещество" для того, чтобы достигать скорости 0,8с;
 BAD-REQ-3 — система должна использовать реляционную базу данных, чтобы
сохранять последние 100 координат мест, в которых брались образцы.

Требования

39

Обратите внимание, что все плохие требования говорят о том, как что-то должно
быть выполнено, а не что система должна делать. Что произойдет, если изменится
расположение памяти? BAD-REQ-1 должен измениться, как и другие требования,
в которых данные зависят от расположения в определенной области памяти. Почему важно использовать именно реактор антиматерии-материи? В конце концов,
ключевым моментом является то, что космический корабль может двигаться с определенной скоростью. И в итоге, важно ли то, что нужно использовать реляционную базу данных для хранения координат? Если посмотреть с точки зрения пользователя, то беспокоить его должно лишь то, что координаты должны быть сохранены.
Для сложных или критически важных с точки зрения безопасности систем (таких
как реальный космический звездолет) требования могут определять реализацию.
В этих случаях не только важно, что система делает нечто, но и то, что она делает
это проверенным и определенным способом. Для большинства систем, однако, такие требования являются излишними и значительно ограничат гибкость в процессе
разработки программного обеспечения. Также станет более сложным тестировать
эти требования, поскольку тестировщику надо определять не только соответствие
наблюдаемого поведения ожидаемому, но и как наблюдаемое поведение происходило.
Создавая требования по особенностям реализации, вы исключаете возможность
тестирования черного ящика. Не зная программного кода, как вы можете быть уверены, что система отображает содержимое памяти по адресу 0x0895BC40? Это
невозможно (по крайней мере, если у вас нет невообразимой суперсилы, позволяющей заглянуть внутрь чипа памяти и понять, что там хранится и где). Все тесты
окажутся тестами белого ящика.

5.1. Тестируемость
С точки зрения тестировщика, одним из самых важных аспектов требований является то, могут ли они быть протестированы или нет. С точки зрения цикла разработки программного обеспечения формулировка "тестируемые требования" является синонимом "хороших требований". Невозможно доказать, что требование, которое нельзя протестировать, выполнено. Давайте рассмотрим пример с двумя
требованиями и постараемся определить, какое из них лучше. Обратите внимание,
что оба они семантически и синтаксически верные и содержат это крайне важное
слово "должен":
1. Система должна увеличивать счетчик ПОСЕТИТЕЛЕЙ на единицу каждый раз,
когда сенсор ТУРНИКЕТА активируется без ошибок.
2. Система должна работать со счетчиком каждый раз, когда кто-то проходит.
Заметим, что первое требование очень определенное; оно обозначает, что должно
быть сделано, какие входные данные отслеживать и какого поведения ожидать.
А именно: каждый раз, когда датчик ТУРНИКЕТА активируется, мы ожидаем, что
счетчик ПОСЕТИТЕЛЕЙ (который может быть переменной, дисплеем или чем-то

40

Глава 5

еще — это следует определить в описании полных требований) увеличивается на
единицу. Второе требование очень неоднозначное сразу по нескольким причинам.
Что делать со счетчиком? Как он узнает, что кто-то прошел? Что означает, что ктото проходит? Невозможно создать тест для такого требования.
Теперь, когда мы увидели примеры тестируемых и нетестируемых требований,
можем ли мы подробно описать, что значит для требования быть тестируемым?
Для того чтобы требования были тестируемыми, они должны соответствовать пяти
критериям, каждое из которых мы рассмотрим отдельно. Им следует быть:
1. полными;
2. последовательными;
3. недвусмысленными;
4. количественными;
5. выполнимыми для тестирования.
Требования должны покрывать всю работу системы. Это то, что мы подразумеваем,
когда говорим, что спецификация требований должна быть полной. Всё, что не покрыто требованиями, будет интерпретировано по-разному разработчиками, дизайнерами, тестировщиками и пользователями. Если что-то важно, оно должно быть
определено точно.
Требования должны быть последовательными, т. е. они не должны противоречить
друг другу или законам Вселенной (или той области, в которой вы работаете). Требования, которые не противоречат друг другу, являются внутренне последовательными; требования, которые не противоречат миру вне системы, называются
внешне последовательными.
Вот пример группы требований, которые не являются внутренне последовательными:
 INCONSISTENT-REQ-1 — система должна отображать сообщение: "ВНИМА-

НИЕ: ИЗБЫТОЧНОЕ ДАВЛЕНИЕ" на консоли, когда давление составляет
100 PSI или более;
 INCONSISTENT-REQ-2 — система должна отключить консоль и не отображать

информацию до тех пор, пока давление меньше 200 PSI.
Что должна делать система, если давление лежит в границах от 100 до 200 PSI?
В данном случае вы можете использовать разбиение на классы эквивалентности,
чтобы определить, что требования не являются внутренне последовательными.
Требованиям необходимо быть недвусмысленными, т. е. они должны определять
всё настолько точно, насколько возможно для той области программного обеспечения, с которой вы работаете. Допустимый уровень однозначности будет значительно отличаться в зависимости от того, какой тип программного обеспечения вы разрабатываете. Например, если вы создаете игру для детей, возможно, достаточно
оговорить, что некая площадь должна быть "красной". Если же вы описываете требования к интерфейсу ядерного реактора, для предупредительного сигнала должен
быть указан точный оттенок красного цвета в цветовой модели Pantone.

Требования

41

С учетом вышесказанного не загоните себя в угол слишком строгими требованиями. Например, если ваши требования говорят, что какая-то страница должна быть
определенного размера в пикселах, у вас могут возникнуть трудности с конвертацией ее для мобильных устройств. Тем не менее требования не должны принимать
следующую форму:
 AMBIGUOUS-REQ-1 — система должна выключать всё, когда нажата кнопка

Выключение.
Что значит "выключать всё"? Разговаривая с друзьями или коллегами, мы можем
использовать такие неопределенные термины, потому что человеческий мозг удивительно хорош в распознании двусмысленностей. Однако двусмысленные требования могут привести к тому, что разработчики или другие заинтересованные лица
могут интерпретировать их по-разному. Классический пример — провал запущенной
NASA миссии Mars Climate Orbiter, когда часть разработчиков использовала имперскую систему мер, а другая — метрическую. Обе группы думали, что правильный
путь получения результатов очевиден, но они использовали различные реализации.
Насколько это возможно, требования должны быть количественными (как противоположность качественным), т. е. если нужно использовать в требованиях численные значения, то используйте их. Следует избегать любых субъективных терминов
вроде "быстрый", "легко реагирующий", "практичный" или "офигенный". Если вам
необходимо указать, что система должна сделать, — указывайте. Например, следующее требование качественное, но не количественное:
 QUALITATIVE-REQ-1 — система должна возвращать результат предельно быстро.
Что мы хотим этим сказать? Тестировать это требование невозможно без определения, что мы понимает под "предельно быстро" и какой результат должен быть
получен быстро.
В итоге должен присутствовать некий здравый смысл при написании требований.
Можно написать требование, которое теоретически можно протестировать, но
в реальной жизни по разным причинам этого сделать нельзя. Такое требование
невыполнимо для тестирования. Скажем, у нас есть следующие требования для
тестирования нашего датчика давления:
 INFEASIBLE-REQ-1 — система должна быть способна выдерживать давление
до 9,5 ⋅ 1011 фунтов на квадратный дюйм;
 INFEASIBLE-REQ-2 — при нормальных рабочих условиях (определенных где-то
в другом разделе) система должна оставаться работоспособной в течение 200 лет
непрерывного использования.
Оба эти требования, конечно, можно протестировать. В первом случае просто поместите систему в эпицентр относительно мощного термоядерного взрыва и определите, ухудшилось ли качество системы после детонации. Второе требование еще
проще: просто нужно ездить с датчиком давления воздуха в колесах 200 лет. Так
как продолжительность жизни человека заметно меньше, вам, вероятно, понадобятся несколько поколений тестировщиков. Передавать ли должность "тестировщик
датчика давления" по наследству или использовать подход "мастер — ученик", зависит только от вас.

42

Глава 5

Когда дело касается физических явлений, становится очевидным, что глупо тестировать такие требования. (Если они не кажутся глупыми вам, сколько тестировщиков вы знаете, у которых в распоряжении имеется ядерное оружие? Вероятно,
меньше пяти.) Однако зачастую более сложно определить, что требование является
невыполнимым, когда вы имеете дело с программным обеспечением. Функционируя в реальном физическом мире, человеческий мозг очень часто сталкивается
с трудностью определения, насколько выполнимым является требование к программе (существующей в виртуальном мире), которое нужно протестировать.

5.2. Функциональное
против нефункционального
Функциональные требования определяют, что система должна делать; нефункциональные требования определяют, какой система должна быть.
Требования, которые мы обсуждали ранее, являлись функциональными, т. е. они
говорили, что система должна выполнять конкретное действие при определенных
условиях. Например:
1. Система должна показывать сообщение "Пользователь не найден", если пользователь попытается авторизоваться и введенное имя пользователя не существует
в системе.
2. После извлечения записи из базы данных, если какое-то из полей неправильное,
система должна возвращать строку "Неверное значение" для этого поля.
3. После запуска система должна отобразить сообщение "ДОБРО ПОЖАЛОВАТЬ
В СИСТЕМУ" в пользовательской консоли.
Функциональные требования являются (относительно) простыми для тестирования;
они говорят, что определенное поведение должно происходить при заданных условиях. Очевидно, будут какие-то сложности и вариации при тестировании некоторых требований, но общая идея очевидна. Например, для второго требования тесты
могут проверять каждое поле базы данных и различные виды неверных значений.
Это может быть довольно запутанный процесс, но существует план, позволяющий
разработать тесты, которые напрямую вырастают из требований.
Нефункциональные требования описывают общие характеристики системы,
в отличие от определенных действий, которые выполняются при определенных
условиях. Нефункциональные требования часто называют атрибутами качества,
потому что они описывают качество системы в противоположность тому, что она
должна делать конкретно. Некоторые примеры нефункциональных требований:
1. Система должна использоваться опытным пользователем компьютера после не
более 3-часового обучения.
2. Система должна быть способной работать с одной сотней пользователей одновременно.
3. Система должна быть надежной с временем непредвиденного простоя меньше
1 часа в месяц.

Требования

43

Нефункциональные требования зачастую более сложны для тестирования, чем
функциональные, потому что ожидаемое поведение оказывается гораздо более неопределенным. Именно поэтому особенно важно ясно определить само требование.
Одним из основных путей четко определить нефункциональные требования является использование в них количественных значений.

5.3. Замечание о наименовании требований
В былые времена, когда примитивные инженеры долбили бинарные деревья каменными топорами, существовал только один путь написания требований. Это был
путь, по которому требования писали их праотцы, а до этого писали праотцы праотцов, и так до самых истоков цивилизации. Этот священный метод наименования
требований был таков.
ТРЕБОВАНИЯ:
1. Система должна делать X.
2. Система должна делать Y.
3. Система должна делать Z, если происходит событие A...
Это было довольно просто для маленьких проектов. Но по мере того, как проект
становился всё больше и больше, в этой схеме начинали возникать проблемы. Например, что произойдет, если требование станет нерелевантным? Тогда возникнут
"пропавшие" требования; в списке требований могут быть разделы 1, 2, 5, 7, 12
и т. д. Наоборот, что если нужно добавить требования к программе? Если нумерация списка требований возрастает линейно, эти новые требования должны быть
размещены в конце или втиснуты между существующими (требование 1.5).
Другой проблемой была необходимость запоминать требования, основываясь исключительно на номерах. Для обычного человека не очень сложно держать в голове список из нескольких требований, но он не сможет это делать, когда требований
к программному обеспечению окажется сотни, если не тысячи.
Было разработано несколько способов для разрешения этой проблемы. Один из них
заключался в группировке всех требований по различным секциям (например,
"DATABASE-1", "DATABASE-2") и продолжении в них традиционной нумерации.
По крайней мере, в такой схеме новое требование для базы данных DATABASE не
понадобится размещать в конце общего списка требований, зато можно будет поместить его вместе с аналогичными требованиями. Также из названия можно догадаться, о чем говорит это требование.
Другой способ — использовать соответствующие аббревиатуры в названиях требований. Префиксы и суффиксы вроде "FUN-" для функциональных и "NF-" для нефункциональных являются довольно обычными.
При этом следует отметить, что еще более важно использовать одинаковый подход
к наименованиям требований внутри команды!

ГЛАВА 6

Тест-планы
Теперь, когда вы узнали про требования, базис теории и терминологию тестирования программного обеспечения, пришло время заняться тест-планами. В тестпланах содержится полный план тестирования тестируемой системы. Затем они
выполняются, т. е. человек или автоматизированный инструмент тестирования
проходит все шаги тест-плана, и его результаты сравниваются с тем, что получено
на самом деле. Иными словами, мы будем проверять соответствие ожидаемого поведения системы (то, что мы написали в нашем тест-плане) тому поведению, которое мы наблюдаем (то, что на самом деле выполняет система).

6.1. Базовая схема тест-плана
Тест-план по своей сути является просто набором тест-кейсов. Тест-кейсы — это
отдельные тесты, которые составляют тест-план. Давайте предположим, что вы
создаете тест-план для тестирования нового приложения, которое говорит вам,
является ли ваш кофе слишком горячим, чтобы его пить. Маркетологи нашего
гипотетического приложения любят холодный кофе и решили не создавать никакой
функционал, сообщающий о том, что температура кофе слишком низкая. Имеются
два требования:
 FUN-COFFEE-TOO-HOT — если измеренная температура кофе составляет 80 °C
или выше, на экране приложения должно отображаться сообщение "СЛИШКОМ
ГОРЯЧО";
 FUN-COFFEE-JUST-RIGHT — если измеренная температура кофе составляет
меньше 80 °C, на экране приложения должно отображаться сообщение "САМОЕ
ТО".
Как мы может разработать тест-план для нашего приложения по измерению температуры кофе? Есть одно входное значение — измеренная температура кофе —
и два возможных выходных, одно из набора ["СЛИШКОМ ГОРЯЧО", "САМОЕ
ТО"]. Мы проигнорируем, что большинство людей посчитают, что кофе температуры 7 °C далеко не "САМОЕ ТО".
Единственное входное значение и одно из двух возможных выходных значений
являются простым случаем разбиения класса эквивалентности, поэтому давайте
разобьем эти классы эквивалентности:

Тест-планы

45

 JUST-RIGHT — [–INF, –INF + 1, ..., 79, 80] → "САМОЕ ТО";
 TOO-HOT — [81, 82, ..., INF – 1, INF] → "СЛИШКОМ ГОРЯЧО".

Нашими граничными значениями являются 80 и 81, т. к. они отмечают разделение
между двумя классами эквивалентности. Давайте также использовать два внутренних значения: 57 °C для класса "САМОЕ ТО" и 93 °C для класса "СЛИШКОМ
ГОРЯЧО". Для этого конкретного примера тест-плана мы проигнорируем неявные
граничные значения бесконечности и отрицательной бесконечности (или, в представлении системы, MAXINT и MININT).
Используя эти значения и общее представление, что мы хотим протестировать, мы
можем начать создавать тест-кейсы. Хотя в разных инструментах и компаниях
применяют различные шаблоны для ввода тест-кейсов, существует относительный
стандарт, который может быть использован или модифицирован для большинства
проектов:
1. Идентификатор, такой как "16", "DB-7" или "DATABASE-DROP-TEST", который однозначно идентифицирует тест-кейс.
2. Тест-кейс — описание тест-кейса и определение, что он тестирует.
3. Предусловия — любые предусловия для состояния системы или мира перед
началом теста.
4. Входные значения — любые значения, напрямую подаваемые на вход в тесте.
5. Шаги исполнения — фактические шаги теста, которые должны быть выполнены тестировщиком.
6. Выходные значения — любые значения, полученные на выходе теста.
7. Постусловия — любые постусловия состояния системы или мира, которые
должны быть истинны после выполнения теста.
Не беспокойтесь, если у вас возникли какие-либо вопросы по поводу этих определений. В следующих главах мы рассмотрим их более глубоко и приведем примеры.

6.1.1. Идентификатор
Как и у требований, у тест-кейсов тоже есть идентификаторы. Они позволяют коротко и быстро сослаться на тест-кейс. Во многих случаях это просто числа, но
можно также использовать более сложные системы наименований, подобные той,
что была описана в разделе об именах для требований.
Тест-планы обычно не такие большие, как требования для программы; когда они
становятся достаточно объемными, отдельные тест-планы группируют в больши́е
тестовые наборы (тест-сьюты, test suites). Зачастую идентификаторы являются
обычными числами. Если вы используете автоматизированное ПО для работы
с тестами, то обычно оно выполняет нумерацию за вас.

46

Глава 6

6.1.2. Тест-кейс (или краткое изложение)
В этом разделе приводится краткое изложение того, что и как должно тестироваться
в тест-кейсе. Таким образом, некто, изучающий тест-план, сможет с ходу сказать,
для чего нужен этот тест-кейс и почему он включен. Обычно это можно понять после внимательного изучения предусловий, входных значений и шагов исполнения,
но для человека проще всего лишь прочитать, что должен делать этот тест.
Примеры:
1. Убедиться, что товары со скидкой могут быть добавлены в корзину и при этом
их цена автоматически уменьшится.
2. Убедиться, что передача нечисловых значений приведет к тому, что функция
вычисления квадратного корня вернет исключение InvalidNumber.
3. Убедиться, что когда система обнаружит достижение внутренней температуры
66 °C, на дисплее отобразится сообщение об ошибке и произойдет выключение
в течение 5 секунд.
4. Убедиться, что если операционная система переключается между временны́ми
зонами во время путешествия, то для отображения результата вычислений используется начальная временная зона.

6.1.3. Предусловия
Для теста зачастую необходимо, чтобы для его запуска система находилась в определенном состоянии. Хотя можно теоретически рассмотреть приведение системы
в данное состояние как часть исполнения теста (см. разд. 6.1.5), чаще гораздо более
разумно сделать, чтобы определенные предусловия были реализованы перед
запуском теста. Это необходимо для многих тестов, которые проверяют не математические чистые функции.
Примеры предусловий:
1. Система запущена.
2. В базе данных содержится запись о пользователе Joe с паролем EXAMPLE.
3. Для флага SORT_ASCEND установлено значение true.
4. В корзине покупателя уже находятся три товара.
Давайте рассмотрим последний пример и поймем, почему лучше иметь выполненное предусловие, чем включать его в шаги теста. Мы хотим протестировать, что
добавление товара в корзину, в которой уже находятся три товара, приведет к отображению информации о четырех товарах в корзине. Читая описание этого теста,
вы можете понять, что добавление первых трех товаров не упоминается. С точки
зрения этого теста шаги по добавлению этих товаров не относятся к делу; всё, что
важно, — это то, что в начале теста три товара находятся в корзине.
С прагматической точки зрения способность устанавливать предусловия добавляет
гибкость и краткость тестам. Предположим, что вместо размещения предусловия
о наличии в корзине трех товаров, мы включим в тест следующие шаги:

Тест-планы

47

1. Найти товар "1XB".
2. Выбрать товар "1XB".
3. Нажать кнопку Добавить в корзину три раза.
Это может сработать, когда вы запускаете тест в первый раз. Но этот тест оказывается очень хрупким — существует множество способов разрушить его, если система вдруг меняется. Что, если товар "1XB" больше не существует? Что, если
у функции поиска имеется дефект и предметы, название которых начинается с "1",
не могут быть найдены? Что, если кнопка Добавить в корзину поменяла название?
Также существует недостаток с точки зрения краткости. Мы только что добавили
три шага в тест, где всего одно предусловие. Краткость, помимо того, что она сестра таланта, очень полезна в гарантировании того, что фокусирование будет идти на
важных частях теста. Шаблонный тест — враг внимания и фокусирования.
Разделительная линия между предусловиями и шагами выполнения иногда может
оказаться скорее искусством, чем наукой. Вообще, чем более критична с точки зрения безопасности наша рабочая область, тем более точными будут предусловия.
Например, допустим, вы тестируете хостинг изображений, где все изображения являются общедоступными и видимыми для всех пользователей. Тест-кейс включает
в себя проверку, что определенное изображение показывается на экране, когда
пользователь открывает соответствующий URL. Для этого теста могут быть достаточны следующие предусловия:
1. Пользователь залогинился.
2. Изображение было загружено по URL /pictures/foo.
Однако если мы тестируем банковское ПО и используем изображение для предупреждения о неправильной транзакции, то здесь, вероятно, будет больше предусловий, а те, что уже имеются, будут более точными:
1. Пользователь X залогинился с паролем Y.
2. У пользователя X в аккаунте нет предупреждений или сообщений о необходимости проверки.
3. Сберегательный счет пользователя X содержит $0,00.
4. Расчетный счет пользователя X содержит $50,00.
5. У пользователя X нет других счетов в банке, кроме сберегательного и расчетного.
6. Пользователь X попытался снять $50,01 с расчетного счета.
В обоих случаях шаги исполнения будут одинаковыми или, по крайней мере, очень
похожими — перейти по URL-адресу и проверить, что отображается определенное
изображение. Однако состояние системы может быть значительно более детализированно в случае использования банковского программного обеспечения. И не
только из-за того, что такая система гораздо более сложная, но и из-за того, что
последствия ошибки для банка окажутся более значительными, чем для хостинга
изображений. В этом случае имеет смысл точно определить, что должно происходить и что должно быть создано перед шагами выполнения. Чем более точно вы

48

Глава 6

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

6.1.4. Входные значения
В то время как предусловия являются теми аспектами системы, которые должны
быть установлены перед запуском теста, входные значения являются теми значениями, которые передаются напрямую в тестируемую функциональность. Это различие может быть трудноуловимым, поэтому давайте рассмотрим несколько примеров.
Представим, что у нас есть алгоритм сортировки billSort, который предположительно в 20 раз быстрее, чем любой другой алгоритм. Не принимая на веру то, что
billSort всегда дает правильный результат, мы разрабатываем тесты для него. В одном из них используется глобальная переменная SORT_ASCENDING. В зависимости от
того, какое значение принимает эта булева переменная — true или false, сортировка будет идти либо по возрастанию (от меньшего значения к большему, т. е. "a",
"b", "c"), либо по убыванию (от большего значения к меньшему, т. е. "c", "b", "a").
Если мы собираемся протестировать этот алгоритм сортировки, установка флага
будет считаться предусловием, т. к. это то, что должно быть установлено перед тестом. Массив ["a", "b", "c"] будет являться входными значениями; эти значения
подаются напрямую для тестирования.
Другой способ понять разницу между входными значениями и предусловиями —
представить себе тесты как методы. В любом случае это хорошее упражнение для
вас — ведь мы займемся этим, когда дойдем до главы о юнит-тестах!
public boolean testArraySort() {
// ПРЕДУСЛОВИЯ
SORT_ASCENDING = true;
// ВХОДНЫЕ ЗНАЧЕНИЯ
int[] vals = [1, 2, 3];
// Новый улучшенный метод billSort! :)
billSorted = billSort(vals);
// Старый унылый метод сортировки из Java :(
NormalSorted = Arrays.sort(vals);
if (Arrays.equals(billSorted, normalSorted) {
// Наши массивы равны, тест пройден
return true;
} else {
// Наши массивы не равны, тест провален
return false;
}
}

Обратите внимание, что вы не используете флаг SORT_ASCENDING в тестируемой функциональности (в частности, в методе billSort()), и, значит, он не является входным
значением. Однако массив vals передается в метод billSort, и поэтому этот массив
рассматривается как входное значение.

Тест-планы

49

Но разве нельзя перестроить систему так, чтобы можно было отправлять флаг в качестве входного значения в метод billSort()?
// Аргументы = массив vals, флаг SORT_ASCENDING
billSorted = billSort(vals, true)

Это, конечно, возможно, и тогда можно рассматривать SORT_ASCENDING как входное
значение, а не предусловие. Является ли что-то предусловием или входным значением — очень часто зависит от реализации программы. Если бы мы программировали на таком языке, например, как Haskell, где побочные эффекты для большинства функций крайне ограничены, то функции, подобные нашей, никогда бы не имели никаких предусловий кроме "программа запущена".
Сортировка является чисто математической идеей, но многое из того, что тестирует
тестировщик, не настолько однозначно. В таких случаях зачастую сложно определить, что считается входными значениями, а что предусловиями. Эвристически,
если кто-то выбирает или вводит значение во время выполнения теста, то это следует рассматривать как входное значение, в ином случае это предусловие. Например, если тест-кейс проверяет авторизацию пользователя с разными именамилогинами, тогда такое имя будет входным значением. Если тест проверяет способность добавлять предметы в корзину после того, как залогинился определенный
пользователь, тогда имя пользователя будет предусловием, т. к. оно не вводится
напрямую в тесте, а авторизация должна быть выполнена перед началом теста.

6.1.5. Шаги выполнения
Теперь, когда и предусловия, и входные значения для тест-кейса определены, пришло время на самом деле запустить тест-кейс. Шаги, предпринятые во время выполнения теста, называются шагами выполнения, и они являются тем, что фактически делает тестировщик. Шаги выполнения очень часто невероятно специфичны,
и весьма критично следовать им в точности. Сравните это с предусловиями, где
достаточно получить конечный результат любыми средствами.
Начнем с простого примера. Мы тестируем интернет-магазин и проверяем, что добавление одного товара в пустую корзину отобразит "1" в качестве количества товаров в корзине. Предусловие заключается в том, что в корзине содержится нулевое количество товаров. Это может быть достигнуто разными путями: пользователь
никогда не логинился раньше; пользователь уже залогинился и купил какое-то количество товаров, что привело к обнулению счетчика; или же любой из находившихся в корзине товаров был удален без совершения покупки. В данном случае не
имеет значения, как этот результат (то есть корзина с нулевым количеством товаров) был достигнут, важно только то, что это выполнено.
С другой стороны, фактические шаги выполнения должны быть изложены предельно понятно:
1. Найти товар "SAMPLE-BOX" путем выбора текстового поля поиска, ввода
SAMPLE-BOX и нажатия кнопки Поиск.

50

Глава 6

2. Должен отобразиться товар с названием SAMPLE-BOX. Нажмите кнопку с текстом "Добавить товар в корзину", расположенную рядом с картинкой SAMPLEBOX.
3. Изучите надпись "Количество товаров в корзине = x" в верхней части экрана.
Обратите внимание, что эти шаги относительно явные. Очень важно записать шаги
достаточно детально, чтобы в случае возникновения проблемы она была легко воспроизводима. Если в шагах выполнения было упомянуто "Добавьте товар", то наш
тестировщик мог выбрать любой товар из доступных. Если проблема в выбранном
товаре (скажем, добавление SAMPLE-PLANT, в отличие от SAMPLE-BOX, никогда не увеличивает количество товаров в корзине), то будет сложно разобраться,
в чем же именно проблема. Соответствующий отчет о дефекте поможет смягчить
эту проблему, но она может быть полностью предотвращена, только если гарантировано, что шаги выполнения определены правильно. Конечно, и здесь можно переусердствовать:
1. Переместите курсор на пиксел (170, 934) путем перемещения правой руки на
0,456 дюйма от предыдущего положения с использованием компьютерной мыши. Эта локация должна соответствовать текстовому полю с меткой "Искать".
2. Примените давление в течение 200 мс к левой кнопке мыши использованием
указательного пальца правой руки.
3. После 200 мс быстро прекратите надавливать на левую кнопку мыши. Убедитесь, что курсор теперь видим и мигает с частотой 2 Гц в текстовом поле...
(и т. д.).
В общем, лучше всего установить уровень спецификации, соответствующий способностям и знаниям людей, которые фактически будут выполнять тесты (или
в случае автоматизированных тестов, программ, которые фактически будут выполнять тесты). Если тестировщики вашей компании хорошо знакомы и с программным продуктом, и с областью, в которой он работает, может оказаться вполне достаточным сказать: "Установи фробинатор в положение ‘FOO’ с использованием
основного диска". Это достаточно определенно, чтобы знакомый с системой пользователь смог однозначно выполнить шаги. Однако не все знакомы с системой так,
как автор тестов. Зачастую те, кто выполняет тесты, являются наемными сотрудниками, аутсорсерами или просто новичками в проекте. Для незнакомого с "фробинизацией" стороннего тестировщика (удивительно, что есть немного незнакомых
с ней людей) может быть необходимо определить, что должно быть выполнено
в деталях:
1. Откройте основную управляющую панель путем выбора Набор > ... > Основной
в меню в верхней части экрана.
2. Выберите фиолетовый диск, обозначенный FROBINATOR. Переместите диск
вправо относительно его начальной позиции до тех пор, пока в поле STATUS не
появится текст FOO.
Да, и в конце надо заметить, что фробинаторов или фробинизации не существует.

Тест-планы

51

6.1.6. Выходные значения
Значения, возвращаемые в ходе тестирования функциональности, называются выходными значениями. Когда дело касается исключительно математических функций, то их очень легко определить — математическая функция по определению
принимает некоторое входное значение (или значения) и отдает некоторое выходное значение (или значения). Например, функция абсолютной величины принимает
некоторое число x; и если x < 0, то она возвращает –x; в противном случае она возвращает x. При тестировании функции с –5 и проверке того, что она возвращает 5,
является очевидным, что входным значением является –5, а выходным — 5. Нет
никаких предусловий; подача –5 должна всегда возвращать 5, и не имеет значения,
какие глобальные переменные установлены, не имеет значения, что именно в базе
данных, не имеет значения, что отображается на экране. Нет никаких постусловий;
функция не должна ничего отображать на экране, записывать что-то в базу данных
или устанавливать глобальную переменную.
Но компьютерные программы не состоят исключительно из математических функций, и поэтому мы должны научиться различать постусловия и выходные значения.

6.1.7. Постусловия
Постусловием является любое условие, которое должно быть проверено после завершения шагов выполнения, и при этом не являться выходным значением. На постусловия тестируемая функциональность может не влиять напрямую, но постусловия напрямую вызываются тестируемой функциональностью. Предупреждающее сообщение на экране, какая-то записанная в базу данных информация, установленная глобальная переменная, продолжение работы системы или то, что поток
не был уничтожен, — всё это примеры постусловий.

6.1.8. Ожидаемое поведение и наблюдаемое поведение
Хотя мы обсудили отличия между выходными значениями и постусловиями, на
самом деле зачастую эти отличия не так важны, или же для понимания этих отличий нужно настолько погрузиться в теорию, что становится очевидным — оно того
не стоит. Подобное можно сказать и о предусловиях и входных значениях.
Главное, что нужно держать в голове при написании тест-кейса:
Когда система находится в состоянии X
И выполняется действие Y,
Я ожидаю, что произойдет Z

Это значение Z и является сутью теста — это ожидаемое поведение. Невозможно
протестировать что-то, если вы не знаете, что должно произойти. Как сказал Льюис
Кэрролл, "если вы не знаете, куда вы идете, то вас приведет любая дорога". Аналогично при написании тест-кейса вам нужно знать, куда должен прийти тест-кейс,
в противном случае невозможно проверить, что система оказалась там, где она
должна быть.

52

Глава 6

6.2. Разработка тест-плана
Перед тем как приступать к написанию любого тест-плана, необходимо задуматься
о конечной цели. Насколько детализированным должен быть тест-план? Какие виды граничных условий должны быть проверены? Какие существуют потенциальные риски неизвестных дефектов? Ответы на эти вопросы будут сильно различаться в зависимости от того, тестируете ли вы детскую онлайн-игру или же программу
для мониторинга ядерного реактора. В зависимости от контекста и области тестируемого программного обеспечения даже для программ со схожими требованиями
могут понадобиться различные стратегии создания тест-планов.
Простейший — и зачастую лучший — путь разработки детализированного тестплана заключается в чтении требований и определении того, как их можно протестировать по отдельности. Обычно довольно много мыслей вкладывается в разработку требований, и поскольку целью системы является удовлетворение требований, имеет смысл убедиться, что все требования были реально протестированы.
Это также открывает простой путь к созданию тест-плана. Итак, первое требование — напишем тест-кейсы; второе требование — напишем еще тест-кейсы; и будем повторять до тех пор, пока все требования не будут покрыты.
Для каждого требования необходимо продумать хотя бы один "счастливый путь" и
создать хотя бы один тест-кейс для этого пути. То есть надо определить, в какой
ситуации при рабочих параметрах будет удовлетворено это требование? Например,
если у вас есть требование, чтобы некая кнопка была доступна для нажатия, только
если значение меньше 10, и недоступна, если 10 и больше, то минимально вам понадобится создать тесты, проверяющие, что кнопка активна, если значение меньше 10, и неактивна, если значение больше либо равно 10. Этим тестируются оба
класса эквивалентности требования (значение < 10 и значение ≥ 10).
Вы также можете продумать случаи, в которых помимо классов эквивалентности
тестируются различные граничные условия. Продолжая рассматривать вышеприведенный пример, давайте предположим, что у вас есть тест-кейсы для значения 5 и
для значения 15, что гарантирует вам использование по крайней мере одного значения для каждого класса эквивалентности. Если вы захотите добавить тест-кейсы,
проверяющие границу между двумя классами эквивалентности, можете добавить
тест-кейсы для 9 и 10.
Сколько таких граничных и угловых кейсов вы добавите и насколько широко вы
протестируете внутренние и граничные значения, будет зависеть от времени и ресурсов, которые у вас есть для тестирования, от области ПО и уровня риска, который допустим для вашей организации. Помните, что исчерпывающее тестирование
во всех отношениях невозможно. Существует скользящая шкала времени и энергии, которые можно потратить на написание и исполнение тестов, и не существует
правильного ответа, что именно нужно выбрать. Нахождение компромисса между
скоростью разработки и обеспечением качества является одной из ключевых задач
тестировщика программного обеспечения.
Создание тест-кейсов для нефункциональных требований (атрибуты качества)
к системе зачастую может быть сложным. Вам следует попытаться убедиться, что

Тест-планы

53

требования сами по себе являются тестируемыми, и оценить количество тесткейсов, которые нужны для этих требований.
К сожалению, просто наличие соответствия между требованиями и тест-кейсами
для каждого их них не всегда означает, что вы разработали хороший тест-план.
Вам, возможно, потребуется добавить дополнительные тесты, чтобы убедиться, что
требования работают в тандеме, или проверить с точки зрения пользователя те ситуации, которые не связаны напрямую с требованиями или не вытекают из них. Более того, вам необходимо научиться понимать контекст, в котором существует программа. Обладание профильными знаниями в области работы поможет вам понять
основные рабочие ситуации, то, как система взаимодействует с внешним миром,
возможные проблемы, и как пользователь может ожидать восстановления системы
после возникновения этих проблем. Если никто в команде не понимает область работы программы, возможно, следует обсудить вопросы со специалистом (subject
matter expert, SME) перед написанием тест-плана.
Понимание программной среды, в которой создается ПО, также может способствовать написанию тест-плана, хотя технически это приведет к тестированию серого
ящика, в отличие от тестирования черного ящика, потому что вы, как тестировщик,
будете знать некоторые особенности внутренней реализации, но это может дать
ценное понимание того, где могут скрываться потенциальные ошибки. Позвольте
мне привести пример. В Java деление на ноль, как показано ниже, выбросит исключение java.lang.ArithmeticException:
int a = 7 / 0;

Не имеет значения, какое у нас делимое, потому что если делитель равен нулю, выбрасывается исключение java.lang.ArithmeticException:
// Во
int b
int c
int d

всех этих случаях выбрасывается одно и то же исключение
= -1 / 0;
= 0 / 0;
= 999999 / 0;

Следовательно, при тестировании написанной на Java программы вы можете предположить, что деление на ноль, по существу, является одним классом эквивалентности; если это произошло, то затем будет происходить одно и то же событие, каким бы оно ни было (например, возможно, что исключение перехвачено и сообщение "Ошибка деления на ноль" выведено в консоль).
JavaScript (да, технически я имею в виду ECMAScript 5 — для тех, кто хочет знать
подробности) не выбрасывает исключение во время деления на ноль. Однако если
знаменатель равняется нулю, то в зависимости от числителя вы можете получить
разные результаты!
> 1 / 0
Infinity
> -1 / 0
-Infinity

54

Глава 6

> 0 / 0
NaN

Деление положительного числа на ноль возвращает бесконечность, деление отрицательного числа — "минус" бесконечность, а деление нуля на ноль — NaN (Not
a Number — не число). Это означает, что деление на ноль, несмотря на то, что является одним "внутренним классом эквивалентности" для Java-программ, оказывается
тремя разными классами для программ, написанных на JavaScript. Зная это, вы,
возможно, захотите протестировать программу и убедиться, что она может обработать все эти возвращаемые значения, и не предполагать, что вы проверили все граничные случаи просто потому, что вы проверили деление на ноль. Это реальный
пример из написанного мною тест-плана, и при его использовании было найдено
несколько дефектов.

6.3. Тестовые фикстуры
Во время написания вашего плана вы можете захотеть протестировать ситуации,
которые сложно воспроизвести. Например, вы можете захотеть проверить, что рассмотренное выше приложение для проверки температуры кофе работает при смене
временны́х зон. Будет бессмысленно дорого реально перемещаться из одной временной зоны в другую. Вспомните, что вы повелитель этого мира тестирования!
Вы можете просто изменить временную зону системы, в которой запущена программа. Если вы тестируете программу, которая будет работать в России, можете
просто поменять настройки локали на Россию вместо того, чтобы срываться на
рейс. Если вам нужны десять пользователей в базе данных для тестирования, можете просто добавить их вручную. Хотя эти фейковые ситуации могут не охватить все
дефекты, способные произойти в реальности, они помогут обнаружить многие
из них.
Скрипт или программа, используемые для перевода тестируемой системы в состояние готовности для тестирования, называется тестовой фикстурой. Текстовые
фикстуры могут быть простыми и состоять из последовательности шагов, которые
нужно добавить в программу, но ничто не ограничивает их сложность. Аппарат для
тренировки посадки на Луну управлялся астронавтами на Земле, и в его работе использовался сложный механизм обратной связи для симуляции лунной гравитации.
Для того чтобы изучить больше примеров о том, как тестирование и тестовые фикстуры помогали астронавтам добраться на Луну, обратитесь к книге Дэвида Минделла "Цифровой Аполлон: Человек и машина в космическом полете" (David
Mindell "Digital Apollo: Human and Machine in Spaceflight").
Тестовые фикстуры часто используются для симуляции внешних систем. История
из личного опыта: я тестировал подсистему, которая взаимодействовала с другими
подсистемами через JSON. Поначалу эти другие системы настраивались вручную
перед каждым тест-кейсом. Вскоре я осознал, что это отнимало много времени и
зачастую приводило к ошибкам. Решением стало использование гема simple_respond
языка Ruby, который принимал заданный JSON-файл и в ответ на любой запрос
всегда возвращал данные этого файла. Вместо того чтобы заниматься настройкой

Тест-планы

55

других подсистем, которые я не тестировал, я мог сконцентрироваться на том, как
будет функционировать моя тестируемая подсистема. Это не только сэкономило
мне время и уменьшило количество зависящих от человеческого фактора ошибок — тесты перестали зависеть от правильной работы других частей системы. Подобные тестовые фикстуры могут также быть переиспользованы при взаимодействии с внешними системами, когда нельзя модифицировать их состояние для заданного тест-кейса.

6.4. Выполнение тест-плана
Выполнение тест-плана называется прогоном (проходом, тест-раном). Прогон
можно представить как эквивалент объекта и класса. Выполнение тест-плана создает прогон подобно тому, как при работе с классом мы можем создать объект. Тестплан является картой, показывающей, куда можно пойти, в то время как прогон подобен путешествию.
Выполнение тест-плана должно быть сравнительно простым процессом, предполагающим, что тест-план разработали надлежащим образом. В конце концов, вы потратили время, чтобы убедиться, что все предусловия выполнимы, что входные
значения заданы, что шаги выполнения достаточно детализированы, а выходные
значения и постусловия могут быть протестированы. На данном этапе выполнение
тест-кейсов будет сравнительно механическим процессом (и это одна из причин
появления автоматизированного тестирования). Вы можете отправить кого-то
в помещение, где находится компьютер, на котором можно запустить программу,
и спустя несколько часов, в зависимости от длительности тест-плана, этот человек
выйдет из помещения с полностью протестированной системой.
К сожалению, это прекрасное видение не всегда становится реальным. В процессе
выполнения тест-кейса этот тест-кейс может получать различные статусы. В итоге
тест-кейс получит некий финальный статус, хотя во время прогона этот статус
будет изменяться. Существуют также статусы "нулевой" или "не протестирован",
которые означают, что этот данный тест-кейс еще не был выполнен.
Хотя не существует универсального хранилища статусов, можно привести репрезентативную выборку тест-кейсов, с которой вы можете встретиться в своей работе
тестировщика. Названия могут меняться, но эти шесть типов обеспечивают хорошее покрытие ситуаций, в которых может оказаться ваш тест-кейс:
1. Пройден (Passed).
2. Неудавшийся (Failed).
3. Остановлен (Paused).
4. Запущен (Running).
5. Заблокирован (Blocked).
6. Ошибка (Error).
Пройденный тест является тем, в котором всё ожидаемое поведение (т. е. выходные значения и постусловия) соответствует наблюдаемому поведению. Проще
говоря, это тест, где всё работает.

56

Глава 6

Напротив, неудавшийся тест является тем тестом, в котором по крайней мере одна
из составляющих наблюдаемого поведения не соответствует ожидаемому поведению. Это различие может быть в выходных значениях или постусловиях. Например, если функция вычисления квадратного корня возвращает значение квадратного корня четырех, равное 322, то этот тест-кейс должен быть помечен как неудавшийся. Если у тест-кейса было постусловие, что на экране должно появиться
сообщение "ОШИБКА: СЛОНЫ НЕ МОГУТ ТАНЦЕВАТЬ", а на экране сообщение об ошибке гласит "ОШИБКА: СЛОНЫ НЕ МОГУТ ВЫБРАСЫВАТЬСЯ ИЗ
ОКНА", то этот тест-кейс также является неудавшимся. Всякий раз, когда тест-кейс
помечается неудавшимся, должен быть зарегистрирован соответствующий дефект.
Это может быть новый дефект или же известный дефект, вызывающий множество
проблем, например ошибки для всех животных с утверждением, что они не могут
выбрасываться из окна, в то время как правильным будет сообщение, что они не
могут танцевать. Если нет дефекта, связанного с неудавшимся тест-кейсом, то либо
тест-кейс не был достаточно важен для тестирования, либо найденный дефект недостаточно важен для документирования. Если это так, вам надо переосмыслить
ваш тест-кейс!
Остановленный тест является тестом, который был запущен, но затем поставлен
на удержание на какой-то период времени. Это позволяет другим тестировщикам и
менеджерам знать статус теста и прогресс, которого достиг тестировщик. Это также гарантирует, что другой тестировщик не начнет выполнять тест, который уже
выполняется. Тест-кейс может быть остановлен по банальным причинам — скажем, тестировщик отправился перекусить или занялся чем-то, связанным с тестируемой системой (например, покинул помещение для получения новых тестовых
данных). В любом случае предполагается, что тестировщик продолжит работу над
тестом после своего возвращения, но не означает, что тест сам по себе не может
быть выполнен (это покрывается статусом "Заблокирован", рассматриваемым
ниже).
Запущенный тест является тестом, который начат, но пока еще не завершен, и таким образом, конечный результат пока неизвестен. Данный статус обычно используется в случаях, когда выполнение теста занимает значительное время и тестировщик хочет дать понять другим тестировщикам, что тест находится в стадии выполнения. Хотя технически все тесты находятся в состоянии "запущен" короткий
период времени (когда тестировщик выполняет шаги выполнения), и если нет какой-либо автоматизации, то этот статус обычно присваивается долго исполняемым
тестам.
Иногда тест невозможно выполнить в данный момент. Причиной этого могут быть
внешние факторы (например, в связи с недоступностью части тестового оборудования) или внутренние (скажем, часть функционала не доработана, или невозможно
тестировать в связи с дефектами, присутствующими в системе). В таких случаях
тест может быть помечен как заблокированный. Это означает, что тест не может
быть запущен в настоящее время, хотя к нему можно вернуться во время будущих
прогонов, когда разрешатся проблемы, препятствующие его запуску.
Наконец, в некоторых случаях тест-кейс просто не может быть выполнен сейчас
или в будущем в связи с проблемой в самом тест-кейсе. В таких случаях статус тес-

Тест-планы

57

та может быть отмечен как "ошибка". Помеченные ошибочными тесты могут
вступать в противоречие с требованиями: например, в требовании может говориться о том, что фон веб-страницы должен быть синим, в то время как тестируемое
приложение является работающей с командной строкой консольной программой.
Проблемой может быть, если, скажем, ожидаемое поведение говорит о том, что
квадратный корень из 25 должен выдавать результат "пудель". Иногда причиной
пометки теста ошибочным может оказаться простая опечатка, но чаще это указывает на фундаментальную проблему с пониманием командой разработчиков или командой тестировщиков разрабатываемого программного обеспечения. Тест-кейсы с
пометкой "ошибка", в отличие от тест-кейсов, помеченных заблокированными, не
должны запускаться, пока эта ошибка не будет устранена.

6.5. Отслеживание тестовых прогонов
Хотя вы можете выполнять тест-план для удовольствия или ради желания самосовершенствоваться, в большинстве случаев вам захочется записать результаты тестплана. Это можно сделать при помощи специального программного обеспечения,
электронной таблицы или даже блокнота. В некоторых случаях это требуется нормативами среды, но даже если не требуется, отслеживание того, какие тесты прошли, а какие нет, будет очень полезным.
При отслеживании тестового прогона есть несколько информационных разделов,
которые вы захотели бы включить:
1. Дата исполнения теста.
2. Имя или другой идентификатор (т. е. логин или ID-номер) тестировщика.
3. Название или другой идентификатор тестируемой системы.
4. Указатель того, какой код тестировался. Это могут быть тег, ссылка, номер версии, номер сборки или какая-то другая форма идентификации.
5. Тест-план, к которому относится тестовый прогон.
6. Итоговый статус каждого тест-кейса. Обратите внимание, что временные статусы, такие как "Остановлен", должны быть изменены на итоговый статус перед
завершением тестового прогона.
7. Список всех дефектов, задокументированных в результате выполнения тесткейса в случае их обнаружения, или же разъяснение причины того, почему тест
имеет иной статус, отличный от "Пройден".
Пример тестового прогона может выглядеть так:
Дата: 21 мая 2014 г.
Имя тестировщика: Jane Q. Tester
Система: Meow Recording System (MRS)
Номер сборки: 342
Тест-план: Тест-план Meow Storage Subsystem

58

Глава 6

Результаты:
TEST 1: Пройден
TEST 2: Пройден
TEST 3: Неудавшийся (записан дефект #714)
TEST 4: Заблокирован (дополнительная функция программы пока не реализована)
TEST 5: Пройден
TEST 6: Неудавшийся (причина в известном дефекте #137)
TEST 7: Ошибка (очевидная ошибка в тест-плане; необходимо проверить с отделом
системного инжиниринга)
TEST 8: Пройден
TEST 9: Пройден

Если тест не проходит, должно произойти одно из двух событий: либо заполняется
информация о новом дефекте, либо отмечается, что причиной проблемы стал уже
известный дефект. Дефект означает, что система не действует, как запланировано;
ожидаемое поведение не соответствует наблюдаемому поведению. Больше о документировании дефектов вы узнаете в следующей главе.
Если тест заблокирован, должно быть указано, из-за чего произошла блокировка.
Причина может лежать вне зоны ответственности команды тестирования (например
это может быть из-за того, что новая возможность программы еще не реализована),
или существует что-то, что может быть улучшено (например, при отсутствии соответствующего оборудования). Объяснение причины незавершенности теста и
включение этого объяснения в результаты тестового прогона будет полезно не
только при документировании статуса, но и позволит руководителям или другим
сотрудникам решить проблему в будущем. Например, если часть тест-плана требует наличия какого-то особого оборудования, то задокументированная информация
о том, что из-за его нехватки нельзя пройти тесты, может побудить руководство
к закупке оборудования или инженеров к поиску альтернативных решений.
Хочется уповать на то, что тесты со статусом "Ошибка" будут редкостью. Впрочем,
если найден ошибочный тест, тестировщику надлежит отметить, почему он или она
считает этот тест ошибочным. Соображения по исправлению (или по крайней мере
по получению дополнительной информации) следует включить в результат данного
тест-кейса.
Отслеживание тестовых прогонов позволяет вам увидеть, где падают ваши тесты.
Если вы заметите, что какая-то часть программы падает чаще других, возможно,
вам следует сфокусироваться на этом. И наоборот, если тесты всегда успешно проходят (и так продолжается годы), они могут быть недостойны вашего внимания.
Также отслеживание поможет вам увидеть, где сбои проявляются время от времени, и таким образом вы узнаете, какие тест-кейсы нестабильны. Просмотр тестовых
прогонов спустя некоторое время не только позволит вам оценить прогресс и качество программного обеспечения, но и даст понимание, как в дальнейшем лучше
создавать следующие тест-планы.

Тест-планы

59

6.6. Матрицы трассируемости
Теперь, когда у нас есть список требований и тест-план для их тестирования, что
еще остается? Конечно, можно отправиться домой и насладиться любимым напитком после того, как целый день маялся в шахтах данных. Но есть кое-что еще, что
можно обсудить в этом разделе. Мы неформально разработали тесты, которые, как
мы полагаем, удовлетворяют требованиям, но мы можем перепроверить, что наши
требования и тест-план синхронизированы, путем создания матрицы трассируемости.
Матрица трассируемости является простым способом определить, какие требования совпадают с тест-планами, и отобразить это на понятной диаграмме. Она состоит из списка требований (обычно просто идентификаторов требований) и списка
номеров тест-кейсов, которые соответствуют этим требованиям (т. е. тех, что тестируют конкретные аспекты данного требования).
В качестве примера вернемся к спецификации требований для приложения по измерению температуры кофе. Вы обратите внимание, что список требований немного изменился — и это нормально при разработке программ!
 FUN-COFFEE-TOO-HOT. Если измеренная температура кофе составляет 80 °C и

выше, то приложение должно отображать на дисплее сообщение "СЛИШКОМ
ГОРЯЧО".
 FUN-COFFEE-JUST-RIGHT. Если измеренная температура кофе меньше 80 °C,

но больше 55 °C то приложение должно отображать на дисплее сообщение
"САМОЕ ТО".
 FUN-COFFEE-TOO-COLD. Если измеренная температура кофе составляет 55 °C

и меньше, то приложение должно отображать на дисплее сообщение
"СЛИШКОМ ХОЛОДНО".
 FUN-TEA-ERROR. Если жидкостью, температуру которой измеряют, является

чай, то приложение должно отображать на дисплее сообщение "ИЗВИНИТЕ,
ЭТО ПРИЛОЖЕНИЕ НЕ ПОДДЕРЖИВАЕТ РАБОТУ С ЧАЕМ".
Мы запишем идентификаторы требований и оставим пространство для идентификаторов тест-планов:
FUN-COFFEE-TOO-HOT:
FUN-COFFEE-JUST-RIGHT:
FUN-COFFEE-TOO-COLD:
FUN-TEA-ERROR:

Теперь давайте посмотрим на завершенный тест-план и определим, какие тесткейсы соответствуют тестированию заданных требований. Для каждого подходящего тест-кейса запишем его идентификатор рядом с требованием:
FUN-COFFEE-TOO-HOT: 1, 2
FUN-COFFEE-JUST-RIGHT: 3, 4, 5
FUN-COFFEE-TOO-COLD: 6, 7
FUN-TEA-ERROR: 8

60

Глава 6

Легко увидеть, что для каждого требования есть по крайней мере один покрывающий его тест. Если бы было другое требование, скажем:
 FUN-COFFEE-FROZEN. Если кофе находится в твердом, а не жидком состоянии, то приложение должно отображать на дисплее сообщение "ЭТОТ КОФЕ
МОЖЕТ БЫТЬ ТОЛЬКО СЪЕДЕН, НО НЕ ВЫПИТ",
и мы попытались бы создать матрицу трассируемости, то было бы легко увидеть,
что для проверки этого требования тестов нет:
FUN-COFFEE-TOO-HOT: 1, 2
FUN-COFFEE-JUST-RIGHT: 3, 4, 5
FUN-COFFEE-TOO-COLD: 6, 7
FUN-TEA-ERROR: 8
FUN-COFFEE-TOO-FROZEN:

Точно так же матрицы трассируемости могут позволить нам определить, есть ли
у нас "бесполезные" тесты, которые не тестируют ни одного из требований. Например, представим, что мы создали "Тест-кейс 9":
ИДЕНТИФИКАТОР: 9
ТЕСТ-КЕЙС: определить, правильно ли приложение показывает температуру пуделя.
ПРЕДУСЛОВИЕ: пудель живой и в добром здравии, с нормальной для пуделя температурой 38 °С.
ВХОДНЫЕ ДАННЫЕ: Нет.
ШАГИ ВЫПОЛНЕНИЯ: навести датчик на пуделя на пять секунд. Прочитать значение
с дисплея.
ВЫХОДНЫЕ ЗНАЧЕНИЯ: Нет.
ПОСТУСЛОВИЯ: на экране демонстрируется сообщение "С пуделем всё в порядке".

В нашей матрице трассируемости снова появляется пробел, но на этот раз на стороне требований. Тест-кейс 9 не соответствует ни одному из требований, и это
будет ненужный тест:
FUN-COFFEE-TOO-HOT: 1, 2
FUN-COFFEE-JUST-RIGHT: 3, 4, 5
FUN-COFFEE-TOO-COLD: 6, 7
FUN-TEA-ERROR: 8
???: 9

Время от времени в "реальном мире" могут найтись тесты, которые не подходят
к какому-либо определенному требованию. Например, если системный инженер не
создал требование по надежности, тест-план все равно может включать тест, позволяющий убедиться, что система работает, даже если она запущена целый день. Это
определенно не наилучшая практика, но такое иногда встречается. Если это произошло, лучшим выходом станет создание требования по надежности, которое
можно протестировать.
Конечно, матрица трассируемости дает очень простой обзор тестового покрытия.
Тот факт, что каждое требование было протестировано, не означает, что каждое
требование было протестировано тщательно. Например, что если у системы есть
проблемы с чрезвычайно горячим кофе? Наибольшая температура, которую мы
проверяли, составляла 93 °С, но проблема может возникнуть при температуре

Тест-планы

61

94 °С. Также в матрице трассируемости нет проверки, что наши тесты хорошие.
Если мы тестировали, соответствует ли система требованию FUN-COFFEE-TOOHOT путем помещения системы в ледяную воду, и при этом утверждаем, что тесткейс соответствует требованию FUN-COFFEE-TOO-HOT, то об этом никак нельзя
сказать по матрице трассируемости.
Матрицы трассируемости являются хорошим способом перепроверить вашу работу
и сообщить другим людям за пределами вашей команды, насколько хорошо покрыта система с точки зрения тестирования. Если время поджимает, вы или ваш менеджер можете решить, что определенные части или функции системы более важны для тестирования, чем другие, и поэтому вы можете даже не писать тесты для
этих менее важных частей. Опять-таки, это не наилучшая практика, но, по крайней
мере, вы можете использовать матрицу трассируемости для отслеживания пробелов
в вашем тестовом покрытии.
Потребителям и менеджменту, особенно в таких зарегулированных отраслях, как
оборона и медицина, также могут потребоваться матрицы трассируемости как
способ доказать, что системы были протестированы по крайней мере на базовом
уровне.

ГЛАВА 7

Ломая программу
До этого момента мы фокусировались на разработке тест-планов и лишь немного
упомянули о вопросе, что именно надо тестировать помимо необходимости удостовериться в соответствии программного обеспечения требованиям. Хотя это не
самый худший подход к тестированию ПО, он пропускает слишком много важных,
связанных с верификацией аспектов, особенно по проверке незаметных граничных
случаев.
Причина тестирования — в поиске дефектов; чтобы найти дефекты, вам нужно решиться сойти со счастливого пути, гарантирующего, будто все вводят данные в том
формате, который вам необходим, у систем неограниченная память и ресурсы процессора, а ваши компьютерные сети никогда не подвержены сбоям. Вам необходимо переместиться в темные леса, где люди пытаются ввести "СОРОК СЕМЬ"
вместо 47, где у программ заканчивается память в разгар вычислений, а злодей из
фильмов ужасов держит в руках заточенный топорик около кабеля компьютерной
сети.
Впрочем, последнее вряд ли возможно. Но вам все же придется подумать о путях
тестирования системы, когда что-то может оказаться не совсем несовершенным.
Рассматривайте эту главу как путь тренировки вашего мышления тестировщика по
созданию проблем и нахождению трещин в тестируемой программе.

7.1. Ошибки, которые следует искать
1. Логические ошибки. Логическая ошибка является ошибкой в логике программы. Разработчик понимал, что нужно сделать, но в процессе преобразования
системы от описания до имплементации что-то пошло не так. Это могло быть
чем-то простым и вызванным случайной заменой "больше чем" (>) на "меньше
чем" ( 3
LinkedList a = new LinkedList( [1,2,3] );
LinkedList b = new LinkedList( [1,2,3] );
// Шаги выполнения - выполнить оператор равенства
boolean result = a.equals(b);
// Постусловия/ожидаемое поведение - утверждение, что
// результат является истинным
assertEquals(true, result);
}

}

Глядя на этот тест, легко найти параллели с ручным тестом. Имеются некие предусловия перед началом теста, определенные шаги выполнения и постусловия, позволяющие сравнить наблюдаемое поведение с ожидаемым. Как упомянуто в одной из
предыдущих глав, ключевой концепцией является понимание, что при тестировании программы некое поведение ожидается при определенных обстоятельствах,
и когда эти обстоятельства наступают, тест должен проверить, что наблюдаемое
поведение фактически является ожидаемым поведением. Хотя юнит-тесты проверяют меньшие модули функциональности, чем большинство тестов черного ящика,
основная концепция остается неизменной.
Когда тест завершается неуспешно, конкретная причина этого будет отображаться
на экране. Мы ожидаем, что тесты должны проходить, поэтому обычно они отображаются просто символом ".". Если вы используете среду разработку или иное
графическое представление теста, неуспешные тесты часто помечаются красным
цветом, а успешные — зеленым.

13.3.1. Предусловия
Перед тем как запустить тест, вам надо определить необходимые предусловия для
теста. Это похоже на предусловия в ручном тесте, только вместо того, чтобы сосредоточиваться на системе в целом, вы сосредоточиваетесь на настройке параметров
для конкретного вызываемого метода. В приведенном выше примере юнит-тест
должен проверить, что в двух связанных списках одинаковые значения (а именно,
1 → 2 → 3) будут рассматриваться как равные при помощи метода .equals объекта
связанного списка. Для того чтобы протестировать на равенство два связанных
списка, сперва мы должны создать два связанных списка и установить для их узлов
одинаковый набор значений. Этот код просто создает списки и помещает их в переменные a и b. Мы будем использовать два этих связанных списка в следующей
фазе юнит-тестирования.

Введение в юнит-тестирование

123

13.3.2. Шаги выполнения
Здесь осуществляется проверка равенства. Мы определяем, является ли a.equals(b)
истинным или нет, и помещаем булево значение результата в переменную result.

13.3.3. Утверждения
Вспомним, что утверждения проверяют соответствие ожидаемого поведения тому,
которое наблюдается при шагах выполнения. А именно, мы используем утверждение assertEquals, которое проверяет, что результирующее значение является истинным (true). Если это так, тест проходит, и мы можем сказать, что при данных условиях метод .equals() работает правильно. Если нет, то тест завершается неуспешно.
Существуют разнообразные утверждения, которые можно использовать. Некоторые из них являются взаимозаменяемыми; например, вместо утверждения, что
result должен равняться true, мы бы могли напрямую утверждать, что результат
является истинным. В Java это выглядит так:
assertTrue(result);

Приведем список наиболее часто используемых утверждений вместе с простыми
примерами использования:
1. assertEquals: утверждает, что два значения равняются друг другу, например
assertEquals(4, (2 * 2)).
2. assertTrue: утверждает, что выражение истинное, например assertTrue(7 == 7).
3. assertFalse: утверждает, что выражение ложное, например assertFalse(2 < 1).
4. assertNull: утверждает, что значение переменной является Null, например для
неинициализированной переменной assertNull(uninitializedVariable).
5. assertSame: утверждает, что переменные не только равны, но и указываются на
один и тот же объект. Например:
Integer a = Integer(7);
Integer b = Integer(7);
Integer c = a;
assertSame(a, b); // Ложное утверждение; сравниваемые значения являются
// одинаковыми, но ссылаются на разные объекты
assertSame(a, a); // Истинное утверждение; одинаковые ссылки на один объект
assertSame(a, c); // Истинное утверждение; разные ссылки на один объект

Кроме того, существует несколько инвертированных версий этих утверждений, таких как assertNotEquals, которые проверяют, что исходное утверждение не является
истинным. Например, assertNotEquals(17, (1 + 1)). По моему опыту, такие утверждения используются гораздо реже. Обычно при возможности хочется проверить
конкретное ожидаемое поведение, а не то, что это не неожидаемое поведение.
Проверка того, что что-то не существует, может быть индикатором того, что тест
хрупкий или непродуманный. Представьте, что вы написали метод, который будет
генерировать романтические стихи XIX века. Вы знаете, что эти стихи не должны

124

Глава 13

начинаться со слова "гомоиконичность", поэтому вы пишите тест, чтобы проверить
это:
@Test
public void testNoLispStuff() {
String poem = PoemGenerator.generate("19th_Century_Romantic");
String firstWord = poem.split(" ");
assertNotEquals("homoiconicity", firstWord);
}

Когда ваши стихи начинаются со слов "Узрите", "Дорогая" или "Ясный", тест
пройдет. Однако он также пройдет, если стихи начинаются с %&*()_ или java.lang.
StackOverflowError. В общем, тест должен искать позитивное поведение, а не отсутствие негативного. Представьте тестирование, проверяющее, что на веб-странице
не появляется сообщение с приветствием. И если на странице отображается сообщение об ошибке 500 Internal Server Error, то тест все равно пройдет. Тщательно
продумывайте случаи сбоев при тестировании на отсутствие определенного поведения.

13.3.4. Обеспечение проверки тестами
того, что вы ожидаете
Один из простейших способов добиться этого — сперва обеспечить, чтобы ваши
тесты проваливались! Хотя мы подробно рассмотрим стратегию разработки, которая всегда требует, чтобы тесты сперва проваливались, в главе 15, посвященной
разработке через тестирование, небольшая правка теста часто может доказать, что
он не проходит успешно все время, потому что, например, вы ошибочно утверждаете, что true == true.
Еще хуже, если вы тестируете что-то совершенно другое, а не то, что, как вам
кажется, вы тестируете. Например, предположим, что вы тестируете возможность
использования отрицательных чисел в вашем новом методе absoluteValue(). Вы пишете тест:
@Test
public void testAbsoluteValueNegatives() {
int result = absoluteValue(7);
assertEquals(result, 7);
}

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

Введение в юнит-тестирование

125

@Test
public void testAbsoluteValueNegatives() {
int result = absoluteValue(-7);
assertEquals(result, -7);
}

Если вы не понимаете, что должно произойти после получения методом отрицательного значения (в данном случае метод должен вернуть значение без отрицательного знака), тогда вы напишете совершенно неправильный метод, который возвращает совершенно неправильное значение, но ваши тесты будут проходить, потому что они будут проверять, будет ли возвращено некорректное значение.
Помните, что тесты — это не магия; им нужно говорить, какое должно быть ожидаемое значение. Если вы ожидаете ошибочное значение и наблюдаемое значение
соответствует ошибочному, тест пройдет. Тесты не безошибочны. Вам по-прежнему необходимо использовать свой ум, чтобы убедиться, что они всё проверяют
правильно.
В некоторых случаях вы можете совсем ничего не тестировать! Тест JUnit падает,
только если ошибочно утверждение. Если вы забыли добавить утверждение, тогда
тесты все равно пройдут, вне зависимости от того, какие шаги выполнения вы
в него добавите.
В рассмотренном ранее тесте сравнения связанных списков что бы вы могли изменить, дабы удостовериться, что ваши тесты тестируют именно то, что вам нужно?
Что произойдет, если вы измените данные первого связанного списка так, что он
будет содержать 1 → 2?
@Test
public void testEquals123() {
LinkedList a = new LinkedList( [1, 2] );
LinkedList b = new LinkedList( [1, 2, 3] );
boolean result = a.equals(b);
assertEquals(true, result);
}

Или 7→ 8 → 9?
@Test
public void testEquals123() {
LinkedList a = new LinkedList( [7, 8, 9] );
LinkedList b = new LinkedList( [1, 2, 3] );
boolean result = a.equals(b);
assertEquals(true, result);
}

Или вы хотите заменить проверку равенства проверкой неравенства?
@Test
public void testEquals123() {
LinkedList a = new LinkedList( [1, 2, 3] );
LinkedList b = new LinkedList( [1, 2, 3] );

126

Глава 13
boolean result = !(a.equals(b));
assertEquals(true, result);

}

Во всех этих примерах тест должен провалиться. Здесь вы можете немного отдохнуть, зная, что ваш тест не является тавтологическим (всегда завершающимся
успешно, вне зависимости от того, что делает код) или тестирующим что-то отличное от того, что, вы думаете, он тестирует.
Вы можете захотеть, чтобы ваш тест сперва провалился, и, таким образом, при его
прохождении вы будете знать, что возникшая при тестировании проблема исправлена. И хотя по-прежнему нет гарантии, что ваши изменения исправили проблему — в конце концов, ваше утверждение может быть неверным! — но это все же
весьма вероятно.

13.4. Проблемы с юнит-тестированием
Юнит-тестирование и техники, которые мы на данный момент изучили, помогут
нам продвинуться довольно далеко, но не пройдут за нас всю дорогу. Просто используя утверждения и код, который мы рассматривали, нельзя проверять, например, что будет выведена конкретная строка, или появится окно, или будет вызван
другой метод..., и много всего важного. В конце концов, если бы все методы возвращали разные значения для различных входных данных, никогда не отображая
их пользователю и не взаимодействуя с окружением, у нас не было бы возможности узнать, как работают наши программы. Нашими единственными выходными
данными стали бы общее увеличение шума вентилятора и нагрев процессора.
Любое поведение помимо возврата значения называется побочным эффектом.
Отображение окна, печать текста, связь с другим компьютером в сети — все это
с терминологической точки зрения побочные эффекты вычислений. Даже присвоение переменной или запись данных на диск являются побочными эффектами.
Функции и методы без побочных эффектов, которые только получают данные из
параметров, называются чистыми. Чистые функции всегда будут возвращать одинаковый результат для одинаковых входных значений и могут вызываться бесконечное количество раз без изменения каких-либо других аспектов системы. Функции и методы, у которых имеются побочные эффекты или которые могут выдавать
различные результаты, зависящие еще от чего-либо помимо переданных в качестве
параметров значений, являются нечистыми. Некоторые языки, такие как Haskell,
проводят четкое разделение между чистыми и нечистыми функциями, но в Java
такого нет.
Примером чистой функции может быть математическая функция, например, вычисляющая квадратный корень, и в Java она будет выглядеть так:
public double getSquareRoot(double val) {
return Math.sqrt(val);
}

Введение в юнит-тестирование

127

Если предположить, что отсутствуют связанные с плавающей запятой ошибки или
подобные, квадратный корень из 25 всегда будет равняться 5, вне зависимости от
того, какие глобальные переменные установлены, вне зависимости, сколько сейчас
времени и какая сегодня дата, вне зависимости от всего. Нет и побочных эффектов
от вызова функции квадратного корня; здесь нет выскакивающего окошка каждый
раз после того, как ваша система сосчитает квадратный корень.
Примером нечистой функции может быть вывод данных о глобальных переменных,
или любой метод, выводящий что-то в консоль или на экран, или зависящий от
переменных, которые специально не передаются. В целом, если вы видите метод,
который ничего не возвращает (void), то он, вероятно, нечистый — чистая функция,
возвращающая тип void, будет абсолютно бесполезной, т. к. возвращаемое значение
является единственным способом взаимодействия с остальной частью программы.
Ниже приведен пример нечистой функции, которая позволяет пользователям отправиться в котокафе (это место, где вы можете попить кофе и погладить котов):
public class World {
public void goToCatCafe(CatCafe catCafe) {
System.out.println("Petting cats at a Cat Café!");
catCafe.arrive();
catCafe.haveFun();
}
}

Чистые функции обычно проще тестировать, т. к. при передаче одинаковых входных значений будут получены одинаковые результаты, а это позволяет легко протестировать вход и выход при помощи стандартных процедур юнит-тестирования.
Нечистые функции являются более сложными, т. к. у вас может не оказаться
выходного значения, к которому можно применить утверждения. Кроме того, они
могут зависеть от частей кода вне этого конкретного метода или модифицировать
их. Приведем пример нечистого метода, который сложно протестировать, т. к. его
зависимости и выходные данные не локализованы. В следующем коде все переменные с префиксом _global определены и установлены как внешние для метода:
public void printAndSave(CatCafe catCafe) {
String valuesToPrint = DatabaseConnector.getValues(_globalUserId);
valuesToSave = ValuesModifier.modify(valuesToPrint);
writeToFile(_globalLogFileName, valuesToSave);
printToScreen(_globalScreenSettings, valuesToPrint);
}

Сравните это с функцией квадратного корня, где мы точно знали, что именно мы
передаем. Так как переданное значение является единственным значением, к которому у функции есть доступ, и единственное значение, которое нас интересует, —
это возвращаемое значение, проверить результаты конкретного расчета довольно
легко. Но в методе printAndSave() присутствуют зависимости от нескольких внешних переменных. При первичном запуске кода все может работать замечательно;
при изменении значений переменных и последующем запуске все может завер-

128

Глава 13

шиться неуспешно. Сложно понять, где нужно осуществлять проверку ожидаемого
поведения. Похоже, код что-то записывает в файл, но нам нужно разобраться с его
именем и расположением, которые зависят от значения переменной в определенный момент времени. Нам также надо понять, что за переменная _globalUserId, для
того, чтобы определить, какие значения будут правильными и как будет все отображаться на экране в зависимости от значения переменной _globalScreenSetting.
В любом из этих случаев на значения переменных способно повлиять множество
внешних факторов, и выходные значения зависят о того, над чем у нас может не
оказаться прямого контроля. В результате все это делает тестирование нечистых
методов более чем сложной задачей.
Это не означает, что нечистые функции плохие! Как мы увидели, они крайне необходимы, если вы хотите сделать что-то помимо нагрева процессора. В конце концов, вывод чего-либо на экран с технической точки зрения является побочным
эффектом. Но поддерживая как можно большее количество функций чистыми и
ограничивая нечистые функции определенными областями, вы делаете тестирование системы более легким. Вы можете представить этот процесс как "карантин"
нечистых функций для того, чтобы понимать, где могут возникнуть трудности
в тестировании.

13.5. Создание тест-раннера
Предполагая, что у нас загружены правильные jar-файлы, можно вручную скомпилировать отдельные классы юнит-тестов, как показано в приведенной ниже команде. Обратите внимание, что версии и расположение jar-файлов в вашей системе могут отличаться.
Компиляция:
javac -cp .:./hamcrest-core-1.3.jar:./junit-4.12.jar FooTest.java

Отметим, что эта команда работает в операционных системах OS X и UNIX. Для
работы в Windows нужно заменить : на ; (java -cp .;./junit-4.12.jar;./hamcrestcore-1.3.jar TestRunner). Если вы используете Windows 7, вам также потребуется
поместить аргумент classpath в кавычки (java -cp ".;./junit-4.12.jar;./hamcrestcore-1.3.jar" TestRunner). Не используйте ~ или другие сокращения при ссылке на
путь, который указывает на расположение файлов junit и hamcrest. Ваши Javaфайлы могу скомпилироваться, но не запуститься, т. к. в javac опция -cp обрабатывает пути иначе, чем в java.
Затем вы можете добавить свой метод public static void main в каждый отдельный
класс для запуска каждого отдельного теста и определить тестовые методы, которые надо запускать.
Но вы можете представить, что это станет очень утомительным по мере того, как
мы будем добавлять новые тесты и новые методы public static void main в каждый
из этих файлов для запуска всех отдельных юнит-тестов. Оказывается, лучшим решением станет создание тест-раннера. Тест-раннер настроит окружение и выпол-

Введение в юнит-тестирование

129

нит набор юнит-тестов. Если вы используете систему сборок или среду разработки,
этот тест-раннер обычно создается и обновляется автоматически. Тем не менее
я считаю полезным показать, как создать собственный тест-раннер, т. к. от вас может потребоваться создание какого-то особого, или вы можете работать над системой, при разработке которой нет среды разработки или задействуется инструмент
cборок, не поддерживающий автоматический запуск тестов.
Приведем пример простой программы тест-раннера, которая будет выполнять все
методы с аннотацией @Test в любом из заданных классов. Если произойдет сбой,
то на экране будет отображена информация о проблемном тесте; в ином случае
программа даст пользователю знать, что все тесты прошли успешно.
import java.util.ArrayList;
import org.junit.runner.*;
import org.junit.runner.notification.*;
public class TestRunner {
public static void main(String[] args) {
ArrayList classesToTest = new ArrayList();
boolean anyFailures = false;
// Добавьте любые классы, которые вы хотите протестировать
classesToTest.add(FooTest. class);
// Провести все добавленные классы через цикл
// и использовать JUnit для их запуска
for (Class c: classesToTest) {
Result r = JUnitCore.runClasses(c);
// Вывести информацию в случае проблем с этим классом
for (Failure f : r.getFailures()) {
System.out.println(f.toString());
}
//
//
//
//
//
if

Если r неуспешная, то был по крайней мере один отказ.
Таким образом установим anyFailures в true - и ее нельзя
изменить обратно на false (никакое количество успешных
тестов не перекроет тот факт, что один из тестов оказался
неуспешным)
(!r.wasSuccessful()) {
anyFailures = true;

}
}
// После завершения проинформировать пользователя о том, все
// ли тесты прошли или же какие-то из них оказались неуспешными
if (anyFailures) {
System.out.println("\n!!! - По крайней мере одна неудача, см. выше");

130

Глава 13
} else {
System.out.println("\nВСЕ ТЕСТЫ ПРОШЛИ");
}
}

}

Этот простой тест-раннер выполнит все тесты в любых классах, добавленных
в список classesToTest. Если вы хотите добавить дополнительные тестовые классы,
просто следуйте шаблону выше и добавьте их в список классов для тестирования.
Затем вы можете скомпилировать и выполнить ваш тестовый набор с использованием следующих команд:
javac -cp .:./hamcrest-core-1.3.jar:./junit-4.12.jar *.java
java -cp .:./hamcrest-core-1.3.jar:./junit-4.12.jar TestRunner

ГЛАВА 14

Продвинутое юнит-тестирование
Хотя вы можете добиться многого, используя полученные в предыдущей главе знания, существуют аспекты юнит-тестирования, которые невозможно реализовать
с помощью этих техник. В этой главе мы продвинемся дальше и изучим, как создавать более сложные тест-кейсы.

14.1. Тестовые двойники
Юнит-тест должен быть локализованным тестом, т. е. он должен проверять конкретный тестируемый метод или функцию и не тревожиться о других аспектах системы. Если тест завершается неуспешно, мы хотим быть уверены, что проблема
находится в коде этого метода, а не в чем-то, от чего зависит этот метод. Программное обеспечение часто взаимосвязано, и конкретный метод, который опирается на другие методы или классы, может работать некорректно, если эти части
кода не работают корректно.
В следующем методе мы позабавимся с утиным прудом. Вызов .haveFunAtDuckPond()
с Duck d будет кормить утку столько раз, сколько указано в переменной numFeedings.
Затем метод вернет объем удовольствия, прямо пропорциональный тому, сколько
раз покормили утку. Утка будет крякать каждый раз при кормлении. Обратите
внимание, что мы кормим нашу утку натуральным кормом для уток. Не кормите
уток хлебом, это вредно для них! Если в качестве параметра передана утка-null или
же количество кормлений равняется нулю или меньше, то возвращается 0 как объем удовольствия (отсутствие уток и отрицательное кормление не являются удовольствием). Давайте также предположим, что реализация Duck дефектная и вызов
метода .quack() приведет к исключению QuackingException:
public int haveFunAtDuckPond(Duck d, int numFeedings) {
if (d == null || numFeedings inSize || (color == false && reduce == false) {
return privateMethod3(image, inSize, outSize);
} else {
// Здесь может быть еще больше условий
// if...then...else if
}
}
// Здесь располагается множество private-методов
}

Оцените множество сложных тестов, которые понадобятся всего для одного этого
метода! Но даже в этом случае вы не фокусируетесь на реальных изменениях изображения. Многие из ваших тестов будут предназначены для того, чтобы убедиться, что был вызван нужный метод!
Кто-то может возразить, что это недостаточно продуманный код и его надо переработать, в идеале с размещением private-методов в классе, скажем, ImageTransformer,

Продвинутое юнит-тестирование

145

где методы могут быть public и легко проверены юнит-тестированием. Не могу не
согласиться с этим. Однако в том-то и дело, что в реальном мире часто встречается
подобный код и тестировщик не всегда может сказать руководству, что компании
надо потратить несколько месяцев на решение технических проблем вместо того,
чтобы добавлять новые возможности. Если вашей целью является тестирование
программного обеспечения, и тестирование качественное, вам, возможно, придется
иногда тестировать private-методы. Для более подробной информации о том, как
это делается в Java, обратитесь к главе 24.

14.8. Структура юнит-теста
14.8.1. Основной план
Юнит-тесты в Java обычно группируются по классам и далее по методам; они отображают структуру программы. Так как юнит-тесты являются тестами белого ящика, которые тесно работают с кодом, нахождение ошибок в кодовой базе на основе
конкретного неуспешного теста оказывается гораздо более простым, чем в интеграционных или ручных тестах.

14.8.2. Что тестировать?
Что именно тестировать, будет зависеть от области использования тестируемого
ПО и количества времени для тестирования, а также от стандартов организации и
прочих внешних факторов. Конечно, это утверждение не укажет вам направление
движения, и подобные предостережения, наверное, могут быть помещены перед
каждым абзацем этой книги. Существуют некоторые эвристические методы, которым можно следовать и которые напрямую отражают какие-то из вопросов, обсуждаемых при разработке тест-плана.
В идеале вам нужно посмотреть на метод и подумать о различных успешных и неуспешных случаях, определить классы эквивалентности, продумать, какие хорошие
граничные и внутренние значения можно протестировать при помощи этих классов
эквивалентности. Вы можете сосредоточиться на тестировании наиболее распространенных случаев, а за редко используемые взяться позже, по крайней мере, на
первых порах. Если вы создаете критически важное для безопасности программное
обеспечение, часто имеет смысл взяться за тестирование отказов, прежде чем проверять "счастливые пути". Лично я часто сперва работаю с базовым сценарием, а
затем думаю о возможных случаях отказа. Зачастую я возвращаюсь обратно, иногда с профайлером, чтобы увидеть, какой код выполняется наиболее часто, и добавить для него дополнительные тест-кейсы. Я могу попробовать создать мысленную
модель того, что вызывается часто вместо использования профайлера. Я определенно подумаю, откуда поступают входные данные в методы. Если они из системы,
над которой у меня нет контроля (например, пользователи — лучший пример систем, над которыми у меня нет контроля), и значения оказываются неожиданными,
я определенно потрачу больше времени на размышления о возможных случаях отказа и проверке граничных значений.

146

Глава 14

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

14.8.3. Утверждайте меньше, называйте прямо
Когда юнит-тест завершается неуспешно, он должен прямо показать, что пошло не
так и где. Тест не должен быть всеохватывающим "один за всех". Правильно написанный юнит-тест выполняется быстро (редко больше нескольких десятков миллисекунд), так что дополнительное время выполнения и работа по написанию большего количества тестов окажутся незначительными по сравнению с тем временем,
которое сэкономят для вас юнит-тесты, сообщив, что именно пошло не так. Юниттест со множеством утверждений показывает отсутствие мысли о том, что именно
этот юнит-тест должен был проверить. Рассмотрим следующий пример:
public class CatTest {
@Test
public void testCatStuff() {
Cat c = new Cat();
c.setDefaults();
assertTrue(c.isAGoodKitty());
assertEquals(0, c.numKittens());
assertFalse(c.isUgly());
assertNull(c.owner());
}
}

В чем окажется причина, если тест завершится неуспешно? Возможно, что недавно
созданная кошка не является хорошим котенком, как следовало бы. Возможно,
произошла ошибка с количеством котят у недавно созданной кошки. Возможно,
что владелец кошки был назначен с ошибкой. Вам придется проверять утверждения
в коде теста, потому что факт провала теста "cat stuff" не скажет вам ни о чем. Если
имя завершившегося неуспешно теста не говорит вам, в чем причина отказа, и не
указывает, где находится проблема, возможно, вы тестируете слишком много
в каждом отдельном тест-кейсе. У меня неоднократно бывало, что я правил тест
программы, основываясь только на названии неуспешного теста, даже не глядя на
его код. Это является следствием хорошо определенных и проработанных тестов.

14.8.4. Юнит-тесты должны быть независимыми
Юнит-тесты не должны зависеть от порядка запуска. То есть тест 2 не должен зависеть от побочных эффектов или результата теста 1. JUnit и другие фреймворки тестирования не запускают отдельные тест-кейсы в предопределенном порядке. Избе-

Продвинутое юнит-тестирование

147

гание зависимостей друг от друга и разрешение каждому тесту выполняться независимо локализует сбои конкретными тестами. Теперь представьте написание следующего кода:
public class Cat {
private int _length;
public Cat(int length) {
_length = length;
}
public int getWhiskersLength() {
return _length;
}
public int growWhiskers() {
_length++;
return _length;
}
}
public class CatTest {
int _whiskersLength = 5;
@Test
public void testWhiskersLength() {
Cat c = new Cat(5);
assertEquals(_whiskersLength, c.getWhiskersLength());
}
@Test
public void testGrowWhiskers() {
Cat c = new Cat(5);
_whiskersLength = c.growWhiskers();
assertEquals(6, _whiskersLength);
}
}

Если мы запустим эти тесты с JUnit, которые выполняются в случайном порядке,
иногда второй тест пройдет, а иногда завершится неуспешно. Почему?
Давайте предположим, что тесты выполняются в том порядке, в котором записаны,
т. е. testWhickersLength() выполняется первым. Переменная _whiskersLength, которая
по умолчанию равняется 5, будет соответствовать начальному значению, устанавливаемому при создании объекта Cat. То есть наше утверждение, что длина усов
(whiskers length) равняется 5, будет правильным. Когда вызывается метод
testGrowWhiskers(), длина усов увеличивается на единицу, и возвращаемое значение
(т. е. новое значение усов) помещается в переменную _whiskersLength, изменяя ее
значение на 6. Так как _whiskersLength равняется 6, утверждение также правильное.
Поздравляем, все тесты прошли!

148

Глава 14

Теперь рассмотрим, что произойдет, когда тесты пойдут в обратном порядке
и testGrowWhiskers() будет выполняться первым, а затем testWhickersLength(). В конце теста testGrowWhiskers() переменная _whiskersLength (которая является переменной
уровня класса и поэтому используется всеми методами) равняется 6. Теперь выполняется testWhickersLength(), но длина усов нового объекта Cat равняется 5, и это
значение не совпадает с _whiskersLength, равной 6. Теперь мы имеем падающий тест,
но такой, который завершается неуспешно время от времени в зависимости от того,
в каком порядке выполняется тест.
Мы можем исправить это, гарантируя, что тесты не зависят друг от друга. Самый
легкий и лучший способ сделать это — устранить любые общие данные между тестами. В нашем случае это означает независимость от переменной уровня класса
_whiskersLength. Давайте перепишем тесты так, чтобы они могли работать независимо.
public class CatTest {
@Test
public void testWhiskersLength() {
Cat c = new Cat(5);
assertEquals(5, c.getWhiskersLength());
}
@Test
public void testGrowWhiskers() {
Cat c = new Cat(5);
int whiskersLength = c.growWhiskers();
assertEquals(6, whiskersLength);
}
}

Хотя для этого примера нашлось относительно простое решение, в других случаях
может оказаться труднее найти зависимости или внести исправления. Каждый раз,
когда вы находите тест, который проходит время от времени, может потребоваться
поиск "спрятанных" зависимостей между тестами. Этими спрятанными зависимостями могут быть общие для методов объекты или переменные, статические переменные класса или другие внешние данные, на которые опираются ваши тесты.
Обратите внимание, что в JUnit возможно определить порядок запуска тестов
путем использования аннотации @FixMethodOrder. Однако постарайтесь избегать ее
использования. Тесты легко могут оказаться в ловушке зависимости друг от друга,
если разрешить им запускаться в определенном порядке.
Существует еще одно преимущество создания тестов без зависимостей от других
тестов. Независимые тесты могут запускаться параллельно с использованием различных ядер процессора или даже полностью на разных машинах. Если выполнение теста зависит от последовательности запуска, тогда вы не сможете запускать их
параллельно. На современной многоядерной машине вы можете выполнять тесты
во много раз быстрее, если они могут запускаться независимо. И хотя точное уско-

Продвинутое юнит-тестирование

149

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

14.8.5. Старайтесь сделать тесты лучше каждый раз,
когда вы их касаетесь
Зачастую приходится работать с устаревшим кодом, в том числе с кодом, который
плохо написан или не имеет хорошего тестового покрытия. Может показаться заманчивым продолжить работать в том же ключе, в каком создавался код, но вы
должны попытаться сделать всё возможное, чтобы улучшить тестирование кода.
Поддерживайте ваш код легко тестируемым и при необходимости создавайте
обертки вокруг кода, который сложно протестировать.
Дискуссия о написании тестируемого кода продолжится в главе 16 "Написание
тестируемого кода" (а что еще можно ожидать в главе с таким названием?)

14.9. Покрытие кода
Покрытие кода скажет вам, какая часть кодовой базы реально выполняется при запуске тестового набора. Так как точное определение того, что означает "какая часть
кодовой базы", может быть сложным, существуют различные виды покрытия кода.
Простейшей формой покрытия кода является покрытие методов; в данном случае
измеряется процент методов, которые вызываются тестами. Например, представим,
что в классе Turtle есть два метода, crawl() и eat():
public class Turtle {
public void crawl() { ... }
public void eat() { ... }
}

Если у нас есть тест, вызывающий crawl(), но нет вызывающих eat(), то значит, что
у нас покрытие кода составляет 50% (был протестирован один из двух методов).
Даже если бы у нас была сотня тестов, вызывающих crawl() и всевозможными путями проверяющих, как ползает черепаха, но ни один не тестировал бы eat(), то
тестовое покрытие у нас все равно составило бы 50%. 100%-ное покрытие кода мы
получим, только когда добавим хотя бы один тест, проверяющий eat().
Более детальной формой покрытия кода является покрытие операторов. Оно измеряет процент программных операторов, которые были выполнены по крайней мере
одним тестом. Программный оператор является наименьшим "кусочком" кода, который можно рассматривать как отдельную часть кода; в Java такие части обычно
разделяются точкой с запятой. Можно привести примеры операторов:
1. int j = 7;
2. System.out.println("Bark!");
3. foo--;

150

Глава 14

Обратите внимание, что "оператор" не означает "строку кода"! Например, в Java
у вас может быть такая строка:
doSomething(k); k++;

В данном случае мы имеем дело с несколькими операторами, которые расположены на одной строке.
Когда разработчики обсуждают "покрытие кода", они обычно имеют в виду именно
"покрытие операторов". По сравнению с покрытием методов использование покрытия операторов предоставляет гораздо более проработанную информацию о том,
какие части кодовой базы фактически были протестированы. Например, давайте
добавим некоторые детали в наш класс Turtle, чтобы мы смогли увидеть, как выглядит код внутри различных методов:
public class Turtle {
CurrentLocation _loc = World.getCurrentLocation();
GroundType _g = World.getGroundType(_loc);
public void crawl() {
if (_g == DIRT) {
move(SLOWLY);
} else if (_g == GRASS) {
eat();
move(MORE_SLOWLY);
} else {
move(EVEN_MORE_SLOWLY);
}
}
public void eat() {
System.out.println("Yum yum");
}
}

С использованием покрытия методов всего один тест для eat() или всего один тест
для crawl() дадут 50% кодового покрытия. За этим остается незамеченным то, что
crawl() намного сложнее, чем eat(), и любой одиночный тест не сможет проверить
различные варианты выхода, в то время как eat() может быть хорошо протестирован всего одним тестом. Также мы не узнаем, какие конкретно строки не были протестированы, — нам придется исследовать код теста, чтобы определить, тестировалось ли то, что черепаха ползет по земле, траве или по чему-то еще. Результат покрытия операторов может точно сказать нам, какие строки никогда не выполнялись
по время тестового прогона, и мы будем точно знать, какие виды тестов необходимо добавить, чтобы убедиться, что каждая строка была протестирована по крайней
мере единожды.
Существуют другие варианты покрытия кода, среди которых покрытие ветвей, измеряющее, какой был протестирован процент условных выражений (условий if,

Продвинутое юнит-тестирование

151

операторов case и т. п.). Однако эти виды покрытий кода обычно используются для
более специализированного тестирования. Гораздо более вероятно, что вы будете
часто сталкиваться с покрытиями операторов и методов.
Если у вас имеется оператор или метод, покрытый тестами, это не означает, что все
дефекты этой части кода были обнаружены этими тестами! Легко представить дефекты, проскальзывающие через покрытие методов. В нашем примере с Turtle если
бы возникли проблемы с черепахой на траве, а наш тест проверял случай, когда черепаха находится на земле, покрытие методов показало бы, что crawl() проверен,
в то время как в нем по-прежнему могли бы прятаться дефекты. На более низком
уровне абстракции, покрытие операторов не проверяет все варианты выполнения
конкретного оператора. Давайте рассмотрим следующий класс с единственным
методом и связанный с ним тест-кейс:
public class Cow {
public int moo(int mooLevel) {
int timesToMoo = Math.ceil(100 / mooLevel);
for (int j=0; j < timesToMoo; j++) {
System.out.println("moo!");
}
return timesToMoo;
}
}
public class CowTest {
@Test
public void mooTest() {
Cow c = new Cow();
int mooTimes = c.moo(20);
assertEquals(5, mooTimes);
}
}

С точки зрения покрытия кода, у нас 100%-ное покрытие кода и 100%-ное покрытие методов — единственный метод класса вызывается из теста, и выполняется каждый оператор метода. Однако вызов метода moo() с переменной mooLevel, равной 0,
вызовет выброс исключения DivideByZeroException. Этот дефект не будет обнаружен
тест-кейсом, несмотря на то, что выполняются все операторы. Проверка всех классов эквивалентности не является "бронебойной защитой", но она поможет исправить подобные ситуации.
Конечно, метрики покрытия кода могут быть еще более обманчивыми, чем в этом
примере. Как только оператор выполняется тестом, этот код считается "покрытым". Ничто не проверяет, что юнит-тесты действительно что-то проверяют. Рассмотрим следующий пример:
public class CowTest {
@Test
public void mooTest() {
Cow c = new Cow();

152

Глава 14
int mooTimes = c.moo(1);
assertTrue(true);
}

}

Этот тест приводит к 100%-ному покрытию кода, но при этом почти ничего не говорит вам о коде. Единственная информация, которую вы можете получить от прохождения теста, — это то, что вызов c.moo(1) не приводит к завершению работы
программы.
Покрытие кода — мощный инструмент, но, как и все в разработке программного
обеспечения, не является универсальным спасательным кругом. Это отличный способ увидеть, какие области кодовой базы нуждаются в дополнительном тестировании, но он не гарантирует, что любой покрытый код непременно свободен от
дефектов. Он даже не гарантирует, что определенная часть кода действительно
была протестирована.
Подобно вашим любимым питательным хлопьям, юнит-тестирование не должно
становиться ни вашим полноценным завтраком, ни всем тест-планом. Юниттестирование прекрасно подходит для проверки отдельных методов и низкоуровневой функциональности, но оно не сильно поможет с пониманием того, как все сочетается. Более того, все усложняется при попытке определить, как должен выглядеть конечный продукт; все отдельные методы могут работать, но совместно они
образуют нечто, что совсем не удовлетворяет требованиям.
При тестировании необходимо помнить о выполнении ручного тестирования, интеграционного тестирования и, в зависимости от ваших нужд, прочих видов тестирования, таких как тестирование безопасности или тестирование производительности.
Полагаться на один конкретный вид тестирования — это лучший способ пропустить важные дефекты.

ГЛАВА 15

Разработка через тестирование
Хотя мы в общем рассмотрели написание юнит-тестов, остался вопрос: как мы интегрируем написание тестов в процесс разработки программного обеспечения?
Раньше тестирование программ было полностью отдельным процессом от написания кода, но сегодня обеспечение качества программы также является работой программистов. И наоборот, тестирование ПО приобрело множество особенностей
разработки; редкий тестировщик не написал ни строчки кода. Даже тестировщики,
занимающиеся ручным тестированием, часто пишут интеграционные скрипты или
нечто подобное.
Разработка через тестирование (test-driven development, TDD) является методологией для написания качественного программного обеспечения с хорошо продуманным набором тестов. Следуя принципам TDD, разработчики будут знать, что тестировать, в каком порядке и как сбалансировать юнит-тестирование и написание
кода для тестируемого приложения. Конечно, это не панацея. В мире программ, как
нам напоминает Фредерик Брукс (Frederick Brooks), не существует "серебряной
пули", которая решит все ваши проблемы. Однако с правильным использованием
TDD можно немного приручить оборотня разработки программного обеспечения.

15.1. Что такое
разработка через тестирование?
Разработка через тестирование является методологией, которая состоит из нескольких ключевых составляющих.
1. Сперва пишутся тесты, затем код. Перед тем как вы начнете размышлять
о том, как сделать что-то, вы подумаете о том, что нужно сделать. Поскольку
ваш код еще не написан, вы можете проверить, что тест изначально завершается
неуспешно (чтобы избежать тавтологических тестов, которые всегда проходят),
и поставить перед собой конкретную цель, к которой вы будете двигаться
(успешное завершение только что написанного теста).
2. Написание только того кода, благодаря которому тест будет завершаться
успешно. Это гарантирует, что вы фокусируетесь на написании нужного кода
вместо того, чтобы тратить время на разработку, возможно, излишнего фрейм-

154

Глава 15

ворка или другого кода. Одним из ключевых преимуществ разработки через тестирование является то, что она позволяет вам сосредоточиваться, обучая вас
следить за целью текущего цикла, а не думать о всевозможных фрагментах кода,
которые вы могли бы написать.
3. Написание только тех тестов, которые тестируют код. Заманчиво писать
тесты только ради их написания; но это правило поможет вам заниматься тестами, которые написаны лишь для разработанного функционала.
4. Короткий цикл оборачиваемости. TDD выделяет быстрые циклы, благодаря которым разработчик придерживается правильного пути и сосредоточивается на
короткой конкретной цели.
5. Рефакторинг ранний и частый. Рефакторинг — процесс изменения кода без
изменений его внешней функциональности. Он может включать в себя как простые действия, вроде изменения имени переменной или добавления комментария, так и сложные, вплоть до изменения ключевой архитектуры или алгоритмов. Рефакторинг отличается от простого написания кода улучшением внутреннего качества кодовой базы без прямого воздействия на внешнее качество.
И хотя рефакторинг не оказывает немедленного влияния на внешнее качество
тестируемой системы, тем не менее зачастую он косвенно воздействует на нее.
Это выражается в том, что код становится более легким для чтения, понимания,
поддержки и модификации. По мере хода процесса разработки рефакторинг
будет упрощать работу программистов.
Вот пример плохо написанной программы. Мы подвергнем ее рефакторингу для
того, чтобы сделать ее более легкой для чтения без модификации ее существующего функционала.
public class Hours {
public static void main(String[] args) {
double chikChirik = 792.34;
try {
chikChirik = Double.parseDouble(args[40 / 3 - 13]);
} catch (Exception ex) {
System.exit(1 * 1 * 1);
}
int kukurigu = 160 % 100;
int gruhGruh = (2 * 2 * 2 * 2 * 2 * 2) - 4;
System.out.println((chikChirik * kukurigu * gruhGruh) + " seconds");
}
}

Первое, на что мы обратим внимание, — здесь совсем нет комментариев. Это делает код сложным для чтения. Также использование болгарской ономатопеи (т. е.
звуков животных) в качестве имен переменных, хотя и интересно с лингвистической точки зрения, не дает вам никакой информации о том, что эти переменные
должны представлять (chik-chirik — это звук, который издают болгарские птицы,
kukurigu — так кричат болгарские петухи, а болгарские свиньи хрюкают gruh-gruh).
Также присутствуют ненужная настройка переменных и усложнение. Например,

Разработка через тестирование

155

переменным kukurigu и gruhGruh присвоены значения 60 после проведения расчетов.
Значение по умолчанию переменной chikChirik, равное 792.34, никогда не используется; зачем тогда установлено именно это значение? Все исключения перехватываются в той части кода, где устанавливается переменная chikChirik; мы же должны
проверять все отдельные случаи сбоев. Некоторые из наших переменных ни разу не
изменяются — они должны быть объявлены константами (в Java это переменные
final). И в итоге имеется возможность вынести часть вычислений в отдельные методы вместо того, чтобы выполнять все в методе main.
Все эти проблемы могут быть исправлены без изменения поведения программы.
Это и есть рефакторинг, а не исправление дефектов. Сейчас код работает сам по
себе, как и задумано, просто дальнейший процесс разработки с ним не будет легким. Давайте исправим некоторые проблемы и посмотрим, как выглядит код после
рефакторинга. Разработка программного обеспечения — очень сложный процесс,
который часто напрягает умы даже лучших программистов, тестировщиков и менеджеров. За все, что мы можем сделать для уменьшения мыслительной нагрузки
себя и будущих членов команды, нам, без сомнения, скажут спасибо!
public class Hours {
final static int MINUTES_PER_HOUR = 60;
final static int SECONDS_PER_MINUTE = 60;
/**
* Получив количество часов, необходимо вернуть количество
* секунд, которые соответствуют этому количеству часов
*/
public static double calculateSeconds(double hours) {
return hours * MINUTES_PER_HOUR * SECONDS_PER_MINUTE;
}
/**
* Если в качестве первого аргумента командной строки указать
* количество часов, то будет выведено количество секунд в этом
* количестве часов. Например, 1 час = 3600 секунд.
* Дополнительные аргументы командной строки игнорируются
*/
public static void main(String[] args) {
double numHours = -1;
try {
numHours = Double.parseDouble(args[0]);
} catch (NumberFormatException nfex) {
// Переданный аргумент не может быть обработан
System.exit(1);
} catch (ArrayIndexOutOfBoundsException oobex) {
// Аргумент не был передан
System.exit(1);
}
System.out.println(calculateSeconds(numHours) + " seconds");
}
}

156

Глава 15

Такой код гораздо легче читать и понимать, хотя его поведение осталось тем же,
что и у исходного кода до рефакторинга. И в такой код гораздо легче вносить изменения. Предположим, мы хотим отображать сообщение об ошибке, если аргумент
не может быть прочитан или проанализирован, а не просто прекращать работу программы. В коде после рефакторинга мы видим различные режимы сбоев и можем
легко добавить любое подходящее сообщение об ошибке в зависимости от проблемы (т. е. "Аргумент не может быть обработан как double" или "Должен быть передан по крайней мере один аргумент"). Возможно, мы захотим модифицировать нашу программу для расчета французского республиканского календаря, в котором
минута состояла из 100 секунд, а час — из 100 минут (всего в сутках было 10 часов). Довольно просто увидеть константы SECONDS_PER_MINUTE и MINUTES_PER_HOUR и
установить соответствующие значения для них. Реализовать подобное в исходном
коде было бы гораздо сложнее.
К сожалению, зачастую к рефакторингу прибегают, когда процесс разработки уже
идет вовсю, и применить его оказывается труднее. Так бывает, если рефакторингом
раньше не занимались или у команды разработки мало опыта в нем. Трудности,
возникающие при рефакторинге, становятся поводом избегать его в дальнейшем.
Это становится самоисполняющимся пророчеством; избегание рефакторинга кода
делает в дальнейшем рефакторинг еще более сложным! Частый рефакторинг становится составляющей процесса разработки и привычкой, а не тем, чем можно заняться, "когда будет достаточно времени" (заметьте, что времени никогда не бывает достаточно).

15.2. Цикл
"красный — зеленый — рефакторинг"
Мы работаем в рамках этих ограничений, которые накладывает цикл "красный —
зеленый — рефакторинг". Одиночный цикл в TDD включает в себя три следующих
шага.
1. Красный. TDD является формой разработки "сперва тесты" (test-first development, TFD), поэтому сначала необходимо написать тест. Разработчик пишет
сбойный тест для нового функционала или для граничного случая, который необходимо проверить. Только что написанный тест — и только этот тест — должен завершиться неуспешно. Если этот только что написанный тест не завершается неуспешно, это означает, для данного функционала код уже написан. Если
другие тесты завершаются сбоем, это означает, что существует проблема с тестовым набором — возможно, связанная с периодическим или недетерминированным сбоем теста — которую нужно исправить перед тем, как двигаться
дальше. Эта фаза называется "красной", потому что многие фреймворки юниттестирования отображают неуспешные тесты красным. Так как красно-зеленый
дальтонизм охватывает немалую часть человеческой популяции, а люди относятся к тем живым существам, которые, наиболее вероятно, займутся программированием, такой выбор цветов кажется не лучшим. Тем не менее мы будем
придерживаться этого принятого соглашения по цветам.

Разработка через тестирование

157

2. Зеленый. Теперь разработчик пишет код для прохождения теста. Эта работа связана только с тем, чтобы тест завершался успешно, и не должна стать причиной
сбоев других тестов. В данный момент возможно появление "уродливого" кода;
задачей является заставить его работать, а не сделать красивым. Если другие
тесты начинают сбоить, это значит, что разработчик ненароком вызвал регрессию и должен исправить это. В конце этой фазы все тесты должны проходить
(быть "зелеными").
3. Рефакторинг. После того как все тесты завершились успешно, разработчик
должен изучить только что написанный код и заняться его рефакторингом. Здесь
возможны как небольшие проблемы, например с "магическими числами" (т. е.
"голыми" значениям в программе, которые не описаны или не представлены в
виде констант, например if (mph > 65) { ... } вместо if (mph > SPEED_LIMIT) { ... }),
так и такие значительные, как плохо проработанный алгоритм. Все это может
быть исправлено в данной фазе, т. к. в предыдущей фазе фокус был на получении правильного результата. Легко запомнить это поможет мнемоническая английская фраза "first make it green, then make it clean" (сперва зеленый, потом чистый). Так как всегда существует запасной вариант в виде правильно функционирующего (но плохо написанного) кода, разработчик может попробовать
различные подходы, не беспокоясь о том, что код может совсем перестать работать. В самом худшем случае код можно вернуть к состоянию конца "зеленой"
фазы и попробовать другой путь его модификации.
После каждого цикла "красный — зеленый — рефакторинг" разработчик может
подумать о добавлении нового функционала и затем начать новый цикл. Такая цикличность будет повторяться до завершения работ над программой. Это путь тестирования, программирования и рефакторинга, который в конечном итоге приведет
к готовому продукту. Его побочным эффектом станет основательный набор тестов,
который имеет непосредственное отношение ко всем функциям программы.
Все это можно переписать в качестве очень простого алгоритма. Таким образом,
мы увидим, как это помогает сосредоточить внимание работающего над программой человека; всегда существует четко определенный следующий шаг:
1. Написать тест для функциональности, которая еще не была создана.
2. Запустить тестовый набор — завершиться сбоем должны только новые тесты.
Если это не так, сперва необходимо понять, почему завершились неуспешно
другие тесты, и исправить эту проблему.
3. Написать достаточно кода, чтобы этот тест проходил, а другие тесты не начинали сбоить.
4. Запустить тестовый набор — если какой-либо тест завершится неуспешно, вернуться к шагу 3; если все тесты прошли, продолжать дальше.
5. Провести рефакторинг написанного кода и/или любого связанного кода.
6. Запустить тестовый набор — если какой-либо тест завершится сбоем, вернуться
к шагу 5; если все тесты прошли, продолжать дальше.

158

Глава 15

7. Если необходимо добавить функциональность, перейти к шагу 1. Если функциональность добавлять не надо, приложение готово!

15.3. Принципы разработки
через тестирование
При написании кода необходимо помнить о нескольких принципах разработки
через тестирование.
 YAGNI (You Ain’t Gonna Need It — тебе это не понадобится). Не пишите код,

который вам не нужен для прохождения тестов! Всегда заманчиво создать красивую абстрактную систему, которая сможет в будущем обрабатывать все
модификации того, чем вы занимаетесь сейчас, но тем самым вы сделаете код
более сложным. Что еще хуже, вы усложните его таким образом, что в дальнейшем это не поможет развитию системы. Избегайте сложности, пока она на
самом деле не окажется нужна. Если у вас есть граничные случаи или классы
эквивалентности, с которыми надо разобраться, сперва добавьте больше тестов.
 KISS (Keep It Simple, Stupid — не усложняй, тупица). Одной из целей TDD явля-

ется гарантирование, что кодовая база остается гибкой и расширяемой, и одним
из главных врагов этих двух целей является сложность. Сложные системы трудно понять и поэтому модифицировать; сложные системы, как правило, подходят
конкретным системам, для которых они были разработаны, и в них трудно добавлять новые возможности или функционал. Сохраняйте ваш код и дизайн простыми и сознательно избегайте добавления дополнительной сложности.
 Fake It ’Til You Make It (Притворяйся, пока не сделаешь). Вполне нормально за-

действовать фейковые методы и объекты в ваших тестах или использовать return 0
в качестве заменителя тела метода. Вы можете вернуться к этим вопросам позже
по мере необходимости с дополнительными тестами и кодом.
 Avoid Slow Running Tests (Избегайте медленных тестов). Если вы работаете

с TDD, вы запускаете по крайней мере три полных тестовых прогона в рамках
итераций цикла "красный — зеленый — рефакторинг". Это минимум, если
предполагать, что ваш код не вызывает проблем с другими тестами и не имеет
собственных дефектов. Если прогон вашего тестового набора занимает две или
три секунды, это минимальная цена за высокое качество, которое предоставляет
TDD; если же на прогон уходит несколько часов, как долго продержатся разработчики, прежде чем бросить все и заняться непосредственно программированием?
 Remember That These Are Principles, Not Laws (Помните, что это принципы, а не
законы). Было бы контрпродуктивным полностью игнорировать то, что еще
должно выполнить программное обеспечение в рамках следующих итераций
цикла "красный — зеленый — рефакторинг". Иногда тест может быть медленным, но необходимым, или же создание полноценного метода окажется таким
же простым, как и добавление его фейковой версии. Хотя необходимо стремиться следовать принципам TDD, я не знаю никого, кто никогда не нарушал бы ни

Разработка через тестирование

159

один из этих принципов (хотя, возможно, это больше относится к тем людям,
с которыми я провожу время).

15.4. Пример: создание программы FizzBuzz
с использованием разработки
через тестирование
Для того чтобы понять, как работает TDD, давайте напишем простую программу
FizzBuzz с его использованием. Напомним, что FizzBuzz работает следующим образом:
1. Печатает все числа от 1 до 100 с определенными исключениями.
2. Если число делится без остатка и на 3, и на 5, вместо числа должно быть напечатано слово "FizzBuzz".
3. Если число делится без остатка на 3, вместо числа должно быть напечатано слово "Fizz".
4. Если число делится без остатка на 5, вместо числа должно быть напечатано слово "Buzz".
Сперва давайте создадим "ходячий скелет" приложения. Предположим, что у нас
уже установлен JUnit или подобный фреймворк тестирования, и у нас нет необходимости развертывать его; всё, что нам надо сделать, — это сгенерировать наш начальный класс FizzBuzz. Так как мы перебираем диапазон значений и принимаем
решение по каждому из них, давайте создадим класс, у которого есть метод main и
метод fizzbuzzify, который возвращает правильную строку для заданного значения.
Забегая вперед, мы знаем, что хотим пройтись по числам от 1 до 100. Следовательно, существуют четыре случая, которые нам хотелось бы протестировать:
1. Должно быть возвращено само число, если оно не делится без остатка на 3 и
на 5.
2. Должна возвращаться строка "Fizz", если число делится без остатка на 3, но не
делится без остатка на 5.
3. Должна возвращаться строка "Buzz", если число делится без остатка на 5, но не
делится без остатка на 3.
4. Должна возвращаться строка "FizzBuzz", если число делится без остатка на 3 и
на 5.
Нашему тестированию помогает то, что функция является чистой — ее возвращаемое значение полностью определяется входным параметром. У нее нет зависимостей от глобальных переменных, нет побочных эффектов в виде вывода и нет
внешних зависимостей. Подача на вход "2" всегда вернет "2" и ничего больше,
подача на вход "3" всегда вернет "Buzz" и ничего больше, и т. д.
public class FizzBuzz {
private static String fizzbuzzify(int num) {

160

Глава 15
return "";
}
public static void main(String args[]) {
}

}

Теперь давайте добавим наш первый тест для первого случая. Первое число, которое не делится без остатка на 3 или на 5, — это 1, поэтому давайте использовать 1
как первое значение для тестирования нашего метода fizzbuzzify():
public class FizzBuzzTest {
@Test
public void test1Returns1() {
String returnedVal = FizzBuzz.fizzbuzzify(1);
assertEquals("1", returnedVal);
}
}

Если мы запустим его, он завершится сбоем — fizzbuzzify возвращает пустую
строку, которая не равняется 1, и таким образом утверждение оказывается неверным. Отлично, это можно легко поправить!
public class FizzBuzz {
private static String fizzbuzzify(int num) {
return "1";
}
public static void main(String args[]) {
}
}

Теперь, когда мы запустим тест, он завершится успешно! Давайте перейдем к следующей фазе и поищем возможности для рефакторинга. В данном случае я не думаю, что они есть; конечно, здесь есть "магическое число" (хорошо, технически —
магическая строка, представляющая число), но что можно сделать? Заменить его
константой NUMBER_ONE? Понятнее от этого не станет.
Давайте добавим второй тест, для 2, который должен вернуть не Fuzz и Buzz, а "2":
public class FizzBuzzTest {
@Test
public void test1Returns1() {
String returnedVal = FizzBuzz.fizzbuzzify(1);
assertEquals("1", returnedVal);
}
public void test2Returns2() {
String returnedVal = FizzBuzz.fizzbuzzify(2);
assertEquals("2", returnedVal);
}
}

Когда мы запустим тест, он ожидаемо завершится сбоем, т. к. наш метод
fizzbuzzify() всегда возвращает 1 и никогда не возвращает 2. Также обратим вни-

Разработка через тестирование

161

мание, что наш первый тест не стал падать после добавления второго теста —
единственным падающим тестом оказался новый тест test2Returns2(). Исправить
код будет довольно просто, так?
public class FizzBuzz {
private static String fizzbuzzify(int num) {
if (num == 1) {
return "1";
} else {
return "2";
}
}
public static void main(String args[]) {
}
}

Теперь все тесты проходят! Мы гении программирования! Похлопаем сами себя по
плечу!
Конечно, такой шаблон нельзя использовать в дальнейшем. Подвергнем код незначительному рефакторингу, чтобы он работал со всеми целыми значениями, а не
требовал нового else if для каждого значения:
public class FizzBuzz {
private static String fizzbuzzify(int num) {
return String.valueOf(num);
}
public static void main(String args[]) {
}
}

Гораздо лучше! Тесты по-прежнему проходят, поэтому мы можем двинуться к новому циклу петли. Давайте добавим тест для fizzbuzzify(3), возвращающего "Fizz":
@Test
public void test3ReturnsFizz() {
String returnedVal = FizzBuzz.fizzbuzzify(3);
assertEquals("Fizz", returnedVal);
}
fizzbuzzify(3) действительно вернет "3", что, конечно, вызовет сбой нашего теста.
Однако это можно быстро исправить!
private static
if (num ==
return
} else {
return
}
}

String fizzbuzzify(int num) {
3) {
"Fizz";
String.valueOf(num);

162

Глава 15

Ура, наши тесты проходят! Впрочем, это не идеальное решение — оно будет работать только с 3, а мы знаем, что должно быть любое число, которое без остатка
делится на 3. Немного рефакторинга, и мы сможем обрабатывать любое число,
делящееся на 3:
private static String fizzbuzzify(int num) {
if (num % 3 == 0) {
return "Fizz";
} else {
return String.valueOf(num);
}
}

Теперь мы можем найти новые идеи для последующих юнит-тестов — возможно,
мы захотим проверить 6, 9 или 3000. Но сейчас давайте двигаться дальше и добавим "Buzz":
@Test
public void test5ReturnsBuzz() {
String returnedVal = FizzBuzz.fizzbuzzify(5);
assertEquals("Buzz", returnedVal);
}

И снова наш тест упал, поэтому добавим дополнительное else в наши условные
выражения:
private static String fizzbuzzify(int num) {
if (num % 3 == 0) {
return "Fizz";
} else if (num % 5 == 0) {
return "Buzz";
} else {
return String.valueOf(num);
}
}

Теперь тесты проходят, и, похоже, рефакторинг уже не требуется. Давайте создадим последний тест для проверки возврата значения "FizzBuzz":
@Test
public void test15ReturnsFizzBuzz() {
String returnedVal = FizzBuzz.fizzbuzzify(15);
assertEquals("FizzBuzz", returnedVal);
}

Этот тест завершится сбоем, т. к. текущий метод вернет "Fizz":
private static String fizzbuzzify(int num) {
if ((num % 3 == 0) && (num % 5 == 0)) {
return "FizzBuzz";
} else if (num % 3 == 0) {
return "Fizz";

Разработка через тестирование

163

} else if (num % 5 == 0) {
return "Buzz";
} else {
return String.valueOf(num);
}
}

Теперь тесты проходят! Мы можем двигаться дальше и заняться рефакторингом —
например, вынести num % 3 == 0 и num % 5 == 0 в отдельные методы, но мы хотели
показать простую схему процесса TDD. Зачастую в реальной разработке шаги оказываются больше, но ключевой принцип, который нужно держать в голове, заключается в поддержании тестов сравнительно конкретными и ориентированными на
определенные выходные значения. Группирование входных и выходных значений
в классы эквивалентности, как обсуждалось ранее, поможет вам определить, что
именно необходимо протестировать и в каком порядке.

15.5. Преимущества TDD
Одним из основных преимуществ TDD является автоматическое создание тестового набора во время разработки. Не нужно беспокоиться о том, чтобы уложиться в
сроки разработки тестового фреймворка и написания теста, потому что сам процесс
разработки создаст их для вас. К сожалению, тестирование часто откладывается до
конца проекта (что означает, что ему будет уделено мало внимания или его даже
постараются избежать). Использование TDD гарантирует, что у вас будет хотя бы
какой-то тестовый набор, даже если остальное тестирование свернуто.
Когда тесты являются частью рабочего процесса, люди помнят об их создании.
Вероятно, вы не думаете каждое утро после пробуждения "о, мне надо запомнить
почистить зубы сегодня"; это просто привычка, которая является частью вашего
утреннего распорядка. Поэтому вы, скорее всего, будете создавать тесты, если вы
занимаетесь этим постоянно и их разработка вошла у вас в привычку. Это определенно здорово, потому что количество тестов коррелирует с качеством кода! Когда
чем-то занимаешься часто, делать это легко. Любые проблемы с тестовым набором
будут найдены быстро, а заняться проблемным частями или сложным для тестирования кодом можно будет раньше.
Еще одним преимуществом является релевантность тестов, которые вы создаете
в рамках TDD, т. к. новые тесты сперва должны завершаться сбоем (подтверждая,
что тест не является избыточным), и должны пройти, прежде чем цикл написания
части кода будет считаться "завершенным". Менее вероятно, что в этом случае
вы напишете тесты, которые являются ненужными или тавтологическими, подвергнете избыточному тестированию одну область или не протестируете достаточно другую.
Если вы пишете тесты по факту, легко попасть в ловушку предположения, что программа делает все правильно. Однако помните, что эти тесты должны сравнивать
ожидаемое поведение с наблюдаемым. Если вы "ожидаете" что-то, что уже "наблюдается", есть риск, что ваши тесты окажутся тавтологическими!

164

Глава 15

TDD заставляет вас двигаться маленькими шагами. Это помогает гарантировать,
что вы не уходите далеко в работе над чем-то. Если вы написали четыре строчки
кода и создали дефект, то в этом случае гораздо проще понять, где вы ошиблись,
чем если бы вы нашли дефект после написания тысячи строк кода. Мне нравится
сравнивать написание кода с пересечением населенной злыми пингвинами Антарктиды, и тесты являются нашими крепостями против пингвинов. Можно легко пересечь целый континент, если через каждый километр или два вы строите очередную
крепость, и всегда есть место для отступления, если у пингвинов появится безумный блеск в глазах. Вы можете выбрать другой путь, возможно, тот, где меньше
пингвинов, и поставить форт там. Переход Антарктиды без строительства противопингвинных фортов окажется безрассудным маневром, потому что любая атака
пингвинов может отбросить вас к кораблю, с которого вы высадились. Написание
больших кусков кода без тестов точно так же не позволяет добиться значительного
прогресса, поскольку любой дефект может привести к тому, что вам придется отказаться от большей части уже написанного вами кода.
Когда вы тестируете код с самого начала, более вероятно, что этот код будет тестируемым. Вы не только научитесь тестировать код в этом конкретном приложении,
т. к. вы занимаетесь этим все время, но вы вряд ли напишете код, который не сможете протестировать. Почему? Ваш код должен проходить уже написанные вами
тесты, поэтому вы будете стараться писать его таким, чтобы он был тестируемым.
Поскольку вы также постоянно добавляете его в кодовую базу, а не рассматриваете
как одну большую "версию", являющуюся одним гигантским блоком, то ваш код
также будет расширяемым. Вы расширяете его с каждым циклом "красный — зеленый — рефакторинг"!
Использование TDD обеспечивает 100%-ное тестовое покрытие или близкое к нему. Хотя покрытие кода не является идеальной метрикой — в коде, полностью покрытом тестами, может прятаться множество дефектов, — оно подтверждает, что
вы по крайней мере единожды проверяете каждую строчку кода. Это намного лучше, чем делать это в массе проектов.
Разработка через тестирование предоставляет структурированный фреймворк для
написания программного обеспечения. Хотя, конечно же, существует немало недостатков (некоторые из них будут перечислены далее) и ситуаций, для которых
данная методика является неоптимальной, TDD открывает вам путь движения вперед. Неукоснительные, но гибкие шаги цикла "красный — зеленый — рефакторинг" дают разработчикам список того, что делать дальше. Вы постоянно добавляете тесты, пишете код, который должен проходить эти тесты, и осуществляете рефакторинг. Когда у вас нет фреймворка, которому нужно следовать, вы можете
потратить много времени на рефакторинг существующего кода, или недостаточно
времени на написание тестов, или слишком много энергии на написание тестов, но
не кода. Как минимум вы должны учитывать, сколько времени и ресурсов вы хотели бы потратить на каждую составляющую. С TDD у вас уже есть готовые ответы,
вам не нужно искать их, и поэтому вы можете спокойно работать над другими вещами. Есть замечательная книга Атула Гаванде "Манифест чек-листа" (Atul
Gawande "The Checklist Manofesto"), в которой объясняется, как наличие чек-листа

Разработка через тестирование

165

(даже такого простого, как "красный — зеленый — рефакторинг") может помочь
вам в разнообразных начинаниях. Программный инжиниринг не является исключением.
Наконец, и самое главное — разработка программного обеспечения с использованием TDD может дать вам уверенность в вашей кодовой базе. Вы идете по канату
с натянутой снизу страховочной сеткой; вы знаете, что программа, которую вы
пишете, может по крайней мере делать то, о чем вы ее попросите. У вас есть защитные ограждения, которые предупредят вас, когда вы вызовете проблемы в других частях приложения. У вас есть опытная команда разработчиков, которая сообщит вам о следующих шагах. Вы не одиноки.

15.6. Недостатки TDD
Как было упомянуто выше, не существует универсального лекарства в разработке
программного обеспечения. У использования TDD много преимуществ, но не во
всех случаях она оказывается подходящей методикой. Знайте о следующих недостатках перед тем, как решиться использовать TDD.
Разработка с TDD означает написание множества юнит-тестов. Если вашу команду
нужно принудить к первостепенному тестированию программного обеспечения,
фокусирование на юнит-тестах может вытеснить другие виды тестирования, такие
как тестирование производительности и системное тестирование. Нужно помнить,
что даже если вы написали много юнит-тестов для метода, реализующего некий
функционал, это не означает, что вы полноценно протестировали данный функционал.
Нет сомнений, что в краткосрочной перспективе написание тестов означает, что на
разработку того же количества функций уйдет больше времени. Конечно, имеются
преимущества в виде улучшенного качества кода. Тем не менее если кто-то ждал
до ночи, прежде чем проект станет доступен для программного класса (хм), то, вероятно, что TDD является не лучшим решением. В этом случае бронебойная программа, которая не соответствует половине требований, оказывается значительно
хуже программы, которая делает все, что должна, до тех пор, пока вы не передадите ей неправильный параметр, не нажмете + или вдруг не начнете слишком тяжело дышать рядом с ней. Но для больших проектов или для проектов без
подобных малых дедлайнов использование TDD или похожей методики часто оказывается более быстрым в долгосрочной перспективе. Можно привести хорошую
аналогию — написание программы без тестов подобно езде на картинге; кажется
быстро, но на самом деле медленно. Написание программы с тестами подобно
управлению реактивным самолетом; кажется медленно, но на самом деле очень
быстро.
Традиционно TDD предоставляет меньше времени для принятия архитектурных
решений. Из-за малого времени цикла "красный — зеленый — рефакторинг"
меньше времени может быть потрачено на дизайн и архитектуру в противоположность разработке кода, реализующего пользовательские истории. Во многих случа-

166

Глава 15

ях, таких как разработка простого веб-приложения, это совершенно нормально.
Архитектура по умолчанию может быть нормальной, и трата лишнего времени на
размышления о ней может оказаться контрпродуктивной. В других случаях, особенно когда дело касается новых видов программного обеспечения или областей
его работы, выбор архитектуры может быть сложным и логически вытекающим,
и имеет смысл потратить больше времени в начале проекта на размышления о ней.
Более того, может оказаться трудным внести изменения в архитектурный дизайн
в процессе разработки. Хотя методика предполагает гибкость, некоторые решения
по дизайну могут потребовать множества модификаций после написания кода.
Будет проще потратить время в начальном цикле разработки ПО для обдумывания,
что необходимо сделать, и не предполагать, что вы сможете внести изменения
позже.
Для некоторых областей приложений разработка через тестирование является определенно ошибочным подходом. Если вы создаете прототип чего-либо и не уверены, каким должно быть ожидаемое поведение, но знаете, что оно может быстро
поменяться и не попасть к потребителю, тогда TDD будет излишним. Постоянно
растущий набор тестов, который обычно служит вам страховочной сеткой, окажется камнем на вашей шее. Если вы пытаетесь разобраться с ожидаемым поведением
по ходу работы, не имеет смысла использовать методику, которая предполагает,
что вы знаете, каким будет ожидаемое поведением. С другой стороны, предельно
критичные для безопасности приложения, такие как системы электропитания или
управления авиационным радиоэлектронным оборудованием, потребуют гораздо
большего объема проработки дизайна и продуманности, чем может обеспечить
TDD. В этом случае TDD может оказаться слишком гибким.
Помните, когда вы пишете автоматизированные тесты, вы в действительности пишете код. Конечно, этот код выглядит немного по-другому, но вы снова и снова
добавляете накладные расходы к вашей кодовой базе. Это еще что-то, что может
пойти не так, что-то, что потребует рефакторинга, что-то, что потребуется обновлять каждый раз после изменений в требованиях или направлении проекта. Хотя
эти накладные расходы часто компенсируются увеличением качества кода приложения, в некоторых случаях может оказаться не так. В особенно маленьких проектах и скриптах может быть быстрее просто выполнить ручное тестирование, чтобы
убедиться, что приложение делает то, что должно, и не тратить время на создание
полноценного фреймворка для этого приложения.
Наконец вы, как инженер, часто начинаете новый проект не с самого начала (т. е.
не с нуля). Часто вы модифицируете или добавляете функции в уже существующее
программное обеспечение, бóльшая часть которого была написана с использованием других методик или парадигм. Если вы начинаете работать над проектом, использующим очень жесткую методику "Водопад" или код которого с трудом поддается тестированию, использование TDD принесет больше проблем, чем преимуществ. Это также может отдалить вас от участников вашей команды или заставить
потратить слишком много времени на разработку ваших функций.

ГЛАВА 16

Написание тестируемого кода
Хотя мы рассматривали тестирование с точки зрения тестировщика, зачастую бывает полезным рассмотреть его с точки зрения кого-то, кто занимается написанием
кода. Как тестировщик, вы можете сделать жизнь разработчика проще, определяя
дефекты и уровень качества программы, программист может сделать жизнь тестировщика проще, обеспечивая легкое тестирование написанного им кода. В современных реалиях разработчик и программист очень часто — один человек, особенно
если речь идет о юнит-тестах. Таким образом, имеет смысл (с точки зрения теории
игр) сделать код максимально тестируемым.
Создавая код тестируемым, вы не только упростите создание тестов, но и получите
ряд других преимуществ. Вы будет стремиться писать хороший модульный код.
Вносить изменения в будущем окажется проще, т. к. у вас будет больше тестов и
вы сможете писать лучшие тесты. Код будет более легким для понимания, и проблем будет меньше, т. к. вы сможете протестировать все граничные случаи. Хотя
вашей целью может быть написание тестируемого кода, в конечном итоге вы будете писать код, который лучше во всех отношениях.

16.1. Что мы понимаем
под тестируемым кодом?
В каком-то смысле, практически весь код тестируемый. Пока вы контролируете
вход и наблюдаете выход, вы можете протестировать любую часть программного
обеспечения. Под тестируемым кодом мы понимаем код, который легко тестировать в автоматическом режиме на множестве уровней абстракции. Это означает,
что написание тестов для систем черного ящика будет простым, что написание
юнит-тестов (для классов, методов и других модулей кода) не окажется неоправданно сложным, а более продвинутое тестирование (такое, как тестирование производительности и безопасности) возможно. Не всякий тестируемый код является
хорошим кодом — можно написать ужасную мешанину из непроизводительного
спагетти-кода, который тем не менее легко тестировать. Однако любой хороший
код является тестируемым кодом. Возможность написать тесты для определенной
части кода — неотъемлемая часть хорошо написанного кода.

168

Глава 16

Давайте представим, что мы тестируем следующий фрагмент кода. Это часть
видеоигры, в которой симулируется движение птицы. Нажатие кнопки заставляет
птицу лететь и изменяет высоту полета и расположение на экране.
public class Bird {
public int _height = 0;
public int _location = 0;
public void fly() {
Random r = new Random();
_height += r.nextInt(10) + 1;
_location += r.nextInt(10) + 1;
Screen.updateWithNewValues(_height, _location);
}
}

Хотя это простой метод, его будет довольно сложно тестировать при помощи
юнит-тестов. В конце концов, здесь нет возвращаемых значений, к которым можно
применить утверждения. Результаты будут проверяться с учетом переменных
уровня класса. Невозможно дать "правильный" ответ, потому что имеется зависимость от генератора случайных чисел, которую невозможно переопределить. Невозможно проверить, что Screen был обновлен без наличия реального действующего
объекта Screen. Всё это означает, что протестировать код в изоляции будет трудно,
и поэтому очень трудно реализовать юнит-тестирование. На протяжении оставшейся части этой главы мы рассмотрим стратегии, которые гарантируют, что наш код
не окажется таким.

16.2. Основные стратегии тестируемого кода
При написании тестируемого кода на уровне юнит-тестирования следует помнить о
двух ключевых концепциях:
1. Гарантирование, что код сегментирован.
2. Гарантирование, что события повторяемы.
Если какой-либо конкретный метод при работе взаимодействует только с несколькими другими частями системы (классами, методами, внешними программами
и т. п.) — в идеале, если на его результаты влияют только значения, передаваемые в
качестве параметров, — тогда его будет сравнительно легко протестировать. Однако если он взаимодействует со множеством других частей, то может оказаться
очень сложно быстро протестировать его. Вы должны убедиться, что работает не
только нужная часть системы, но и те, от которых она зависит. Вы можете использовать тестовых двойников, но если код писался без их учета, это может оказаться
невозможным. Давайте рассмотрим некий код, который не сегментирован, и поэтому его крайне сложно протестировать:
public int getNumGiraffesInZoo() {
String animalToGet = "Giraffe";

170

Глава 16
try {
numGiraffes = adbw.getNumAnimals(animalToGet);
} catch (DatabaseException dbex) {
numGiraffes = -1;
}
return numGiraffes;

}

Мы понизили число зависимостей до одного класса AnimalDatabaseWorker и вызываем
только один его метод.
Вторая концепция заключается в гарантировании, что всё, что вы делаете, является
повторяемым. Вам определенно не захочется иметь тест, который работает правильно время от времени. Если есть проблема, вы должны знать о ней немедленно.
Если проблемы нет, ложных срабатываний быть не должно.
Вы можете сделать тест повторяемым, если все значения, от которых он зависит,
могут быть реплицированы. Это одна из (многих, многих, многих) причин, по
которой глобальные переменные — в общем, Плохая Идея. Давайте рассмотрим
тестирование следующего метода:
public int[] sortList() {
if (_sortingAlgorithm == "MergeSort") {
return mergeSort(_list);
} else if (_sortingAlgorithm == "QuickSort") {
return quickSort(_list);
} else {
if (getRandomNumber() % 2 == 0) {
return bubbleSort(_list);
} else {
return bogoSort(_list);
}
}
}

Что произойдет, если мы запустим его? Поток выполнения изначально зависит от
двух не передававшихся переменных, поэтому если мы хотим добиться повторяемости нашего теста, нам перед тестированием необходимо выполнить дополнительные проверки, чтобы убедиться, что это правильные значения. Кто знает, какая
часть кода изменила переменные _list и _sortingAlgorithm?! Даже если мы знаем
заранее, что все переменные установлены правильно, как можно тестировать то,
что произойдет, если _sortingAlgorithm будет установлена в значение, отличное от
"MergeSort" или "QuickSort"? Метод вызовет либо bubbleSort(), либо bogoSort(), и нет
(разумного) способа заранее определить, какой именно из них. Если ошибка
в bubbleSort(), но в не в bogoSort(), тест может случайным образом время от времени
завершаться неуспешно.
Для эффективного тестирования код должен быть сегментирован и написан таким
образом, чтобы гарантировать постоянство прохождения определенных частей кода
с определенными значениями. Если это так, тогда каждый раз будут получены оди-

172

Глава 16

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

16.4. Написание тестов заранее
В идеале вам необходимо использовать парадигму TDD или что-то подобное. Даже
если вы не используете TDD, вы должны писать множество тестов примерно в то
же самое время, когда вы пишете код. Вы быстро поймете это, когда окажется, что
написанный вами код не может быть протестирован и вы не можете дальше создавать код, с тестированием которого возникнут проблемы, когда вы займетесь этим
"позже". Обратите внимание, что "позже" часто означает "никогда".
Чем дольше вы будете писать код без подготовки тестов для него, тем более вероятно, что вы создадите код, который сложно протестировать. Даже если вы предполагаете, что он будет тестируемым, зачастую вы не догадываетесь, что написанное
вами будет сложно протестировать по какой-либо из причин. Написание тестов
дает вам подтверждение, что вы идете по правильному пути.

16.5. Пусть ваш код будет DRY
Акроним DRY означает "Don’t Repeat Youself" ("Не повторяйся") и является ключевым принципом того, что ваш код будет не только тестируемым, но и просто
лучше. Простейшим примером создания кода, который не соответствует DRY,
является копирование с незначительным изменением имени метода:
public int[] sortAllTheNumbers(int[] numsToSort) {
return quickSort(numsToSort);
}
public int[] sortThemThereNumbers(int[] numsToSort) {
return quickSort(numsToSort);
}

Если вы думаете, что это нелепый надуманный пример, то я лично видел и исправлял подобный код. Хотя в данном случае все кажется очевидным, потому что методы располагаются рядом, дублирующий код любит прятаться в маленьких закоулках. Подобное случается, когда у вас класс состоит из тысяч строчек кода. Программисту нужен метод, и он быстро добавляет его вместо того, чтобы поискать, не
существует ли уже такой метод; кто-то создает "тестовую" версию метода, а потом
забывает о ней, или же кто-то просто начинает копировать фрагменты кода, найденные онлайн. Если все продолжает работать нормально, то для поиска подобных
проблем очень мало стимулов, не говоря уже об их устранении. И даже если кто-то

174

Глава 16
int numAnimals = DatabaseInterface.execute(
"SELECT COUNT(*) FROM " + animalType + "s WHERE BreedID = " + breedId);
return numAnimals;

}
public int
return
}
public int
return
}

getNumberOfCats(String catBreed) {
getNumAnimals("Cat", catBreed);
getNumberOfPigeons(String pigeonBreed) {
getNumAnimals("Pigeon", pigeonBreed);

Теперь код гораздо более гибкий и поддерживаемый. Добавление новых животных
потребует лишь добавления методов, которые будут вызывать getNumAnimals() с соответствующими параметрами. Прямое тестирование базы данных, которое может
быть довольно сложным, теперь ограничено одним конкретным методом. Энергию
и усилия можно направить на тестирование более абстрактного метода, чем рассредоточивать тестирование по нескольким методам с широкими обязанностями.

16.6. Внедрение зависимости
Как мы видели, использование жестко прописанных зависимостей в ваших методах
может стать причиной сложности их тестирования. Давайте предположим, что
у вас есть ссылка уровня класса на объект Duck, созданный объектом Pond. У класса
Pond имеется метод sayHi(), который произносит "Hi!", приветствуя всех животных
в пруду:
public class Pond {
Duck _d = new Duck();
Otter _o = new Otter();
public void sayHi() {
_d.say("Hi!");
_o.say("Hi!");
}
}

Как это протестировать? Нет простого пути, позволяющего проверить, что утка получила сообщение "Hi!". Внедрение зависимости (dependency injection) позволит
вам избежать этой проблемы. Хотя этот термин звучит солидно и позволяет сойти
за специалиста по тестированию, на самом деле это довольно простая концепция,
с которой вы наверняка сталкивались в своей практике. В нескольких словах, внедрение зависимости означает передачу зависимостей в качестве параметров методу,
в отличие от жесткого прописывания ссылок на них. Благодаря этому гораздо легче
передавать тестовые двойники или моки.
Давайте перепишем класс Pond так, чтобы он позволял внедрение зависимостей:
public class Pond {
Duck _d = null;
Otter _o = null;

176

Глава 16

16.8. Работа с чужим унаследованным кодом
Не у каждого была возможность прочитать отличную книгу с главой, посвященной
написанию тестируемого кода. Многие существующие кодовые базы были написаны людьми, незнакомыми с современными техниками программного инжиниринга
либо по незнанию, либо потому, что эти техники не были распространены в то время. Было бы глупо ожидать от людей, писавших код на FORTRAN IV в 1966 году,
использования техник тестирования, которые получили распространение в девяностые годы. Код, который используется в производстве, но не соответствует передовым практикам современной разработки программного обеспечения и часто имеет
некачественное — или даже не имеет — автоматизированное тестовое покрытие,
известен как чужой унаследованный код (legacy code).
Если сказать кратко, работать с таким кодом трудно. И это не обойти. Вы не сможете использовать множество преимуществ хорошего тестового набора; вы не
сможете взаимодействовать с авторами-разработчиками в случае проблем, неоднозначностей или недокументированного кода; и может быть трудно понять устаревший код, написанный в старом стиле. Однако такое происходит часто, особенно
если вы работаете с компанией, сотрудники которой пишут код довольно давно.
Не имеет смысла переписывать миллионы строк кода каждый раз, когда вы хотите
добавить новую функцию в используемый вами программный набор.
Когда вы обнаружите, что работаете с чужим унаследованным кодом, самым важным для вас станет создание тестов фиксирования (pinning tests). Тесты фиксирования являются автотестами, обычно юнит-тестами, которые проверяют существующее поведение системы. Обратите внимание, что существующее поведение не
всегда является ожидаемым или правильным поведением. Цель теста фиксирования — посмотреть, как ведет себя программа перед внесением в нее каких-либо
изменений. Очень часто оказывается, что странные граничные случаи в действительности используются теми, кто работает с системой. И не в ваших планах непреднамеренно воздействовать на эти случаи при добавлении новой функции, если
только вы специально не решили это сделать. Внесение непреднамеренных изменений может оказаться опасным и для вас, и для пользователей программы. Обратите внимание, это не означает игнорирование вами факта, что программа работает
неправильно. Однако ее исправление следует рассматривать как процесс, отдельный от создания тестов фиксирования.
При работе с чужим унаследованным кодом вы должны ясно представлять функции, которые вы добавляете, и дефекты, которые вы исправляете. Легко начать исправлять каждую ошибку, которую вы видите, но и так же легко создать проблемы.
Вы начинаете забывать, что изначально начинали исправлять, вы не сосредоточены
на чем-то одном и вносите изменения в массивные скопления вместо того, чтобы
двигаться последовательно "шаг за шагом".
Лично мне нравится держать открытым небольшой тестовый файл c изменениями,
которые я хотел бы внести в будущем, но которые в настоящее время находятся
за рамками моей работы. Например, я редактирую класс, чтобы добавить новый

178

Глава 16

дать тестовый двойник или использовать фейковую базу данных в вашем тесте.
Если вам потребуется добавить столбец, необходимо редактировать строку внутри
метода. Теперь давайте сравним этот метод с методом, который является швом:
public int executeSql(DatabaseConnection db, String sqlString) {
return db.executeSql(sqlString);
}

Если вы хотите использовать соединение со второй базой данных, просто передайте ее в качестве параметра при вызове метода. Если вы хотите добавить столбец,
можно просто изменить строку. Даже при отсутствии документации можно проверить граничные и угловые случаи вроде выбрасывания исключения или возврата
конкретного статуса ошибки. Так как вы можете исследовать эти случаи без непосредственного изменения какого-либо кода, будет гораздо проще определить, что
вы ничего не сломали при написании тестов. В конце концов, вы ведь ничего не
изменили! Если вы должны вручную редактировать код для получения наблюдаемого поведения, как в первом примере, любые полученные из тестов фиксирования
результаты должны показаться подозрительными. Модификации кода не всегда
оказываются такими простыми, как в примере выше. Вам может потребоваться отредактировать множество методов, и вы никогда не сможете быть полностью уверены, были бы результаты такими же при выполнении исходного кода, или же они
стали итогом тех правок, которые вы внесли при попытках тестирования.
Обратите внимание, что наличие шва никак не означает, что код хороший. В конце
концов, возможность передавать для выполнения произвольный SQL-запрос является довольно большой угрозой безопасности, если он не был очищен где-либо
еще. Написание кода с большим количеством швов может быть излишеством
и определенно увеличит кодовую базу. Просто швы позволяют вам начать выяснять, как система в настоящий момент реагирует на входные данные. Поиск швов
позволит вам легко начать писать всеобъемлющие тесты фиксирования для того,
чтобы убедиться, что вы охватываете множество граничных случаев.
И возможно, еще более важно, что с психологической точки зрения вы "не поддаетесь" кодовой базе и не опускаетесь на ее уровень. Если для какой-то функции нет
юнит-тестов, то это не означает, что можно заниматься правками без добавления
юнит-тестов. Если для кода какого-то класса нет комментариев, это не дает вам
карт-бланш для того, чтобы не делать комментариев для вашего кода. По мере развития код имеет тенденцию скатываться к наименьшему общему знаменателю. Вы
должны активно бороться с этой деградацией. Пишите юнит-тесты, исправляйте
ошибки по мере их нахождения и документируйте код и функции, как это необходимо.
Если вам интересно более подробно ознакомиться с работой с чужим унаследованным кодом, обратите внимание, что существуют (по крайней мере) две замечательные книги по этой теме. Первая — это "Рефакторинг: Улучшение существующего
кода" Мартина Фаулера (Martin Fowler "Refactoring: Improving the Design of Existing
Code"), а вторая — "Эффективная работа с унаследованным кодом" Майкла Физерса (Michael Feathers "Working Effectively with Legacy Code"). Последняя окажется

ГЛАВА 17

Попарное и комбинаторное тестирование
Представьте, что вы тестируете текстовый редактор, а именно тестируете добавление эффектов к шрифтам, т. е. делаете их полужирными, курсивными, трехмерными, в верхнем индексе и т. д. Конечно, в любом достойном внимания текстовом
редакторе эти эффекты могут быть объединены в одной области теста, так что у вас
может быть слово, написанное одновременном полужирным и курсивом, или буква
в верхнем индексе и трехмерная, или даже предложение, которое выделено полужирным шрифтом, курсивом, подчеркнуто, трехмерное и в верхнем индексе. Число
возможных комбинаций — от абсолютного отсутствия эффектов (т. е. обычного
текста) до использования всех возможных эффектов — равняется 2n, где n равно
количеству эффектов. Таким образом, если имеется 10 различных эффектов шрифта, то общее количество тестов, которые вам потребуется запустить, чтобы проверить все возможные комбинации шрифтов, равняется 210, или 1024. Это нетривиальное количество тестов для создания.
Если вы действительно хотите осуществить тестовое покрытие, вам придется протестировать каждую из всех этих различных комбинаций (т. е. только полужирный;
полужирный и курсив; полужирный и верхний индекс; полужирный, верхний индекс, курсив, зачеркнутый, трехмерный; и т. д.). Представьте, что возникла бы проблема, когда буква отображается только курсивом, подчеркнутой, полужирной,
в верхнем индексе и зачеркнутой. Вы не сможете найти этот дефект, пока у вас не
появится время для выполнения всеохватывающего теста по всем комбинациям!
Тем не менее, оказывается, что такая ситуация встречается реже, чем вы думаете.
Вам может не понадобиться тестировать все эти комбинации, чтобы найти большой
процент дефектов в системе. На самом деле, согласно публикации "Практическое
комбинаторное тестирование" Рика Куна (Rick Kuhn "Practical Combinatorial
Testing"), до 90% всех дефектов в программной системе могут быть найдены простым тестированием всех комбинаций двух переменных. В нашем примере, если бы
вы протестировали все возможные пары (полужирный и курсив, полужирный и
верхний индекс, верхний индекс и трехмерный, и т. д.), вы нашли бы множество
дефектов в программе, но использовали только часть времени тестирования. Из
всех программных проектов, которые были проанализированы, максимальное количество переменных, взаимодействие которых вызывало дефект, оказалось равным шести. Помните об этом при создании тестов, вы можете найти практически

182

Глава 17

пар, но когда количество станет довольно большим, нам придется заняться подсчетами. Здесь потребуется математическое разъяснение.

17.1. Перестановки и комбинации
Возможно, вы забеспокоились, что название книги оказалось обманчивым и "дружеское" знакомство будет заполнено множеством математических операций. Однако не бойтесь — математика здесь сравнительно проста для понимания, а уравнения сведены к минимуму.
Перед тем как мы обсудим базис комбинаторного тестирования, необходимо понять, что такое перестановки. Перестановка — это возможное расположение множества объектов. В него не добавляются и из него не удаляются элементы, только
изменяется их порядок. Это можно сравнить с тасованием колоды карт; каждый раз
при перемешивании карт возникает очередная перестановка, т. к. изменяется порядок, но карты при этом не добавляются и не удаляются. В качестве примера рассмотрим массив X, содержащий следующие значения: [1, 12, 4]. Список возможных перестановок выглядит следующим образом:
1, 12, 4
1, 4, 12
12, 1, 4
12, 4, 1
4, 1, 12
4, 12, 1

Количество возможных перестановок набора равняется факториалу r, или r!, или
r ⋅ (r – 1) ⋅ (r – 2) ⋅ ⋅⋅⋅ ⋅ 1. Например, 3! = 3 ⋅ 2 ⋅ 1 = 6, а 5! = 5 ⋅ 4 ⋅ 3 ⋅ 2 ⋅ 1 = 120.
Также можно выяснить, сколько можно получить различных упорядоченных подмножеств.
Предположим, что r и n являются положительными целыми значениями, количество r перестановок заданного набора n определяется следующей формулой:
P ( n, r ) =

n!
.
( n − r )!

Будет полезным изучить эту тему на примере и не только понять, как всё считается,
но и как это может быть полезно в реальном тестировании. Давайте предположим,
что мы тестируем игру про волейбол "Черепаха", в которой любые две из четырех
черепах будут играть друг против друга. Имена черепах — Алан, Боб, Шарлин и
Дарлин. Мы хотим убедиться, что для любого заданного матча игра будет работать
правильно, и для нас также важен порядок. То есть если Боб играет с Аланом, и Боб
является подающим, то это будет считаться другой игрой по отношению к той,
в которой они играют вместе, но Алан подает первым.
Здесь будет P(4, 2) возможных матчей (т. е. две перестановки). Помещая значения
в уравнение выше, мы увидим, то, что нам необходимо:

184

Глава 17

Функция C(n, r) часто описывается как "n выбирает r". Например, "10 выбирает 4"
будет означать, сколько четверных комбинаций существует в наборе из 10 элементов (210).
Давайте вернемся к нашей волейбольной игре с четырьмя черепахами. В этом случае, однако, нас не будет беспокоить, какая черепаха подает первой — когда Алан
в игре с Бобом подает первым равносильно случаю, когда первым подает Боб. Для
того чтобы определить количество тестов, необходимых для тестирования, нам
нужно определить количество двойных комбинаций для набора из четырех элементов, или C(4, 2), или 6 тестов.
1. Алан/Боб.
2. Алан/Шарлин.
3. Алан/Дарлин.
4. Боб/Шарлин.
5. Боб/Дарлин.
6. Шарлин/Дарлин.
Идея с факториалом становится понятной, когда вы рассматриваете возможные
результаты. Обратите внимание, что наша первая черепаха, Алан, должна взаимодействовать с тремя другими черепахами (всеми остальными черепахами). Вторая
черепаха, Боб, должна взаимодействовать с двумя черепахами, т. к. уже существует
пара, где она с Бобом. Когда мы переходим к Шарлин, мы должны взаимодействовать только с одной черепахой, т. к. Шарлин уже взаимодействует с Аланом и
Бобом. В конце концов, Дарлин уже взаимодействует со всеми другими черепахами, поэтому дополнительных тестов для нее добавлять не нужно.
Обратите внимание, что для проверки всех возможных комбинаций требуется
меньше тестов, чем для перестановок. Оказывается, что число комбинаций, необходимых для полного тестирования, будет расти сравнительно медленно с увеличением n. Пользуясь преимуществом концепций комбинаторики, мы сможем значительно сократить количество тестов, которые нам придется выполнить для получения представления о качестве конкретной программы.
Перестановки и комбинации являются частью области, известной как дискретная
математика, в которой имеется множество приложений для разработки ПО в целом. Большинство специалистов в области компьютерных наук пройдут хотя бы
один курс по дискретной математике в течение своей карьеры.

17.2. Попарное тестирование
Теперь, когда мы понимаем базовые концепции комбинаторики, давайте применим
их при разработке плана попарного тестирования. Для каждой из этих пар мы хотим получить полную таблицу истинности из двух элементов, охватывающую все
возможности. Например, чтобы гарантировать, что мы тестируем все возможные
комбинации полужирного и курсива, можно составить следующую таблицу истинности:

186

Глава 17

моисключающими, нам нужно добавить два теста. Давайте добавим Test 2 для
ложь/истина и Test 3 для истина/ложь. Наш окончательный тест-план будет выглядеть так:

1.
2.
3.
4.
5.
8.

Полужирный | Курсив | Подчеркнутый
-----------+-------+--------------ложь
| ложь | ложь
ложь
| ложь | истина
ложь
| истина | ложь
ложь
| истина | истина
истина
| ложь | ложь
истина
| истина | истина

Мы выполнили требование нашего строгого менеджера и уменьшили количество
тестов на 25%. Мы использовали покрывающий массив (covering array), чтобы
определить кратчайший путь проверки всех двусторонних (попарных) взаимодействий. Покрывающий массив является массивом, представляющим комбинации
тестов, охватывающих все возможные n-сторонние взаимодействия (в данном случае двухсторонние). Обратите внимание, что поскольку исходный массив, который
охватывал все возможные значения таблицы истинности, также покрывал все возможные комбинации этих значений, то технически это тоже покрывающий массив.
Однако в общем случае этот термин относится к уменьшенной версии массива,
который по-прежнему содержит все тесты, необходимые для проверки различных
n-сторонних взаимодействий.
Созданный нами покрывающий массив на самом деле не является оптимальной
конфигурацией для тестирования всех комбинаций, т. к. мы использовали наивный
алгоритм и не проверяли другие возможности. Вероятно, отбирая различные тесты,
при наличии выбора мы могли бы охватить то же количество комбинаций с меньшим количеством тест-кейсов. Подбор вручную является чрезвычайно трудоемким
занятием, особенно с увеличением количества взаимодействий и переменных. По
этой причине шаблоны комбинаторного тестирования часто создаются с использованием специальных программных инструментов. Хотя рассмотрение конкретных
алгоритмов и эвристики находится за рамками данной книги, эти инструменты сгенерируют для вас покрывающие массивы быстро и автоматически. Эти комбинаторные генераторы тестов, такие как бесплатный Advanced Combinatorial Testing
System Национального института стандартов и технологии (National Institute of
Standards and Technology, NIST), оказываются бесценными, когда необходимо сгенерировать покрывающие массивы для нетривиального числа переменных.
Используя алгоритм IPOG в программе NIST ACTS, я смог сгенерировать следующий оптимальный покрывающий массив. В нем всего четыре теста, и практически
невозможно смоделировать более эффективную ситуацию, поскольку каждая попарная таблица истинности сама по себе потребует четырех различных тестов. Еще
более важно, что мы превзошли ожидания менеджера и сократили число тестов на
50%, а не на требуемые 25%, и при этом по-прежнему тестируем все попарные
взаимодействия:

Попарное и комбинаторное тестирование

1.
4.
6.
7.

187

Полужирный | Курсив | Подчеркнутый
-----------+--------+--------------ложь
| ложь | ложь
ложь
| истина | истина
истина
| ложь | истина
истина
| истина | ложь

Давайте проверим, что все возможные пары существуют в тест-плане:
1. Полужирный/курсив: ложь/ложь обрабатывается в тест-кейсе 1, ложь/истина
в тест-кейсе 4, истина/ложь в тест-кейсе 6 и истина/истина в тест-кейсе 7.
2. Курсив/подчеркнутый: ложь/ложь обрабатывается в тест-кейсе 1, ложь/истина
в тест-кейсе 6, истина/ложь в тест-кейсе 7 и истина/истина в тест-кейсе 4.
3. Полужирный/подчеркнутый: ложь/ложь обрабатывается в тест-кейсе 1, ложь/истина в тест-кейсе 4, истина/ложь в тест-кейсе 7 и истина/истина в тест-кейсе 6.
Хотя данный пример реализован с булевыми переменными, можно использовать
любые конечные переменные. Если у переменной возможно бесконечное количество значений (скажем, переменная длина строки), вы можете задать определенное
количество значений (т. е. "a", "abcde" или "abcdefghijklmnop"). При этом необходимо сперва подумать о различных классах эквивалентности, если таковые имеются, и убедиться, что у вас имеется значение для каждого класса эквивалентности.
Также вам следует проверить различные граничные случаи, особенно те, которые
могут вызвать проблемы при совместном взаимодействии с другими переменными.
Предположим, что в нашем предыдущем примере мы хотели бы проверить не символ, а слово. Мы может добавить различные варианты слов, которые необходимо
протестировать. Начнем с "a" (одинарный символ) и "bird" (простое слово). Наш
сгенерированный покрывающий массив будет похож на предыдущий, просто используются "a" и "bird" в качестве значений переменной word вместо "истина" и
"ложь", используемых для других переменных:

1.
2.
3.
4.
5.
6.

word | Полужирный | Курсив | Подчеркнутый
-------+------------+--------+------------"a"
| истина
| ложь | ложь
"a"
| ложь
| истина | истина
"bird" | истина
| истина | ложь
"bird" | ложь
| ложь | истина
"bird" | истина
| ложь | истина
"a"
| ложь
| ложь | ложь

Обратите внимание, что мы по-прежнему проверяем все пары. Давайте проверим
пару word/полужирный, чтобы убедиться, что это так. В Test 1 мы проверяем слово
"a" c включенным полужирным шрифтом. В Test 2 мы проверяем слово "a" c выключенным полужирным шрифтом. В Test 3 — слово "bird" c включенным полужирным шрифтом и в Test 4 — слово "bird" c выключенным полужирным шрифтом. А вдруг нас обеспокоит, что спецсимволы вызовут проблемы с форматированием? Нам необходимо добавить третий вариант для переменной word, что

188

Глава 17

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

1.
2.
3.
4.
5.
6.

word | Полужирный | Курсив | Подчеркнутый
-------+------------+--------+------------"a"
| истина
| ложь | ложь
"a"
| ложь
| истина | истина
"bird" | истина
| истина | ложь
"bird" | ложь
| ложь | истина
"!@#$" | истина
| истина | истина
"!@#$" | ложь
| ложь | ложь

В качестве примечания отметим, что сейчас мы тестируем еще больше парных
комбинаций, а у нас по-прежнему осталось шесть тестов.

17.3. n-сторонние взаимодействия
Хотя многие ошибки обнаруживаются при проверке всех парных взаимодействий,
часто нам хотелось бы пойти дальше и проверить ошибки в трехстороннем, четырехстороннем или даже в большем количестве взаимодействий. Для этого работает
та же теория: должны быть созданы тесты, охватывающие всю таблицу истинности
для каждого трехстороннего взаимодействия переменных. Подобно тому, как мы
проверяли все четыре комбинации значений истина/ложь при тестировании каждого двустороннего взаимодействия в первом примере, мы проверим, что тестируются все восемь комбинаций значений для каждого трехстороннего взаимодействия.
Давайте расширим количество переменных форматирования в нашей системе и добавим в качестве возможного написания текста верхний индекс (или надстрочный)
и перечеркивание. Для того чтобы всеобъемлюще протестировать все варианты,
нам потребуется 25, или 32 теста. После создания покрывающего массива для всех
парных взаимодействий мы получим тест-план для шести случаев. И снова обратите внимание, что, несмотря на то, что в данном случае мы тестируем еще больше
переменных, количество тестов осталось прежним. В данном случае мы запускаем
всего лишь 18,75% тестов, которые потребовались бы нам для исчерпывающего
тестирования, но мы по-прежнему тестируем все парные взаимодействия:

1.
2.
3.
4.
5.
6.

Полужирный | Курсив | Подчеркнутый | Надстрочный | Перечеркнутый
-----------+--------+--------------+-------------+-------------истина
| истина | ложь
| ложь
| ложь
истина
| ложь | истина
| истина
| истина
ложь
| истина | истина
| ложь
| истина
ложь
| ложь | ложь
| истина
| ложь
ложь
| истина | ложь
| истина
| истина
ложь
| ложь | истина
| ложь
| ложь

Тестирование всех возможных трехсторонних взаимодействий потребует больше
тестов. Если вы задумались об этом, значит, это логически необходимо. Так как
существует восемь возможных комбинаций для каждого трехстороннего взаимо-

Попарное и комбинаторное тестирование

189

действия, вполне возможно покрыть любую комбинацию с использованием всего
шести тестов.
Полужирный | Курсив | Подчеркнутый | Надстрочный | Перечеркнутый
-----------+--------+--------------+-------------+-------------1. истина
| истина | истина
| истина
| истина
2. истина
| истина | ложь
| ложь
| ложь
3. истина
| ложь | истина
| ложь
| истина
4. истина
| ложь | ложь
| истина
| ложь
5. ложь
| истина | истина
| ложь
| ложь
6. ложь
| истина | ложь
| истина
| истина
7. ложь
| ложь | истина
| истина
| ложь
8. ложь
| ложь | ложь
| ложь
| истина
9. ложь
| ложь | истина
| истина
| истина
10. истина
| истина | ложь
| ложь
| истина
11. истина
| истина | истина
| истина
| ложь
12. ложь
| ложь | ложь
| ложь
| ложь

Давайте рассмотрим заданное трехстороннее взаимодействие для полужирного/курсива/подчеркнутого и перепроверим, что мы тестируем все возможности.
Ложь/ложь/ложь покрывается Test 12; ложь/ложь/истина покрывается Test 9;
ложь/истина/ложь покрывается Test 6; ложь/истина/истина покрывается Test 5; истина/ложь/ложь покрывается Test 4; истина/ложь/истина покрывается Test 3; истина/истина/ложь покрывается Test 10; истина/истина/истина покрывается Test 1. Обратите внимание, что имеется восемь комбинаций и десять тестов, поэтому имеем
повторы (например, истина/истина/истина покрывается Test 1 и Test 11).
Число взаимодействий можно увеличивать, насколько вам хочется, хотя, если вы
планируете тестирование n-сторонних взаимодействий, где n равняется количеству
имеющихся переменных, вы просто займетесь исчерпывающим тестированием. Согласно эмпирическим исследованиям NIST максимальное количество вызывавших
ошибку взаимодействий равнялось шести, поэтому проверка c большим значением
взаимодействий во многих ситуациях окажется чрезмерной.

17.4. Работа
с большими наборами переменных
Комбинаторное тестирование неплохо справляется с небольшими наборами данных, экономя нам большой процент времени путем снижения количества необходимых тестов. Однако снижение количества тестов с 32 до 12 не так впечатляет;
в конце концов, и 32 теста, вероятно, могут пройти за приемлемое время. А как
комбинаторное тестирование работает для больших наборов переменных и возможных значений?
Ответ — невероятно хорошо. Давайте предположим, что у нас вместо пяти булевых
переменных имеется пятьдесят. Для того чтобы всеобъемлюще протестировать все
возможные комбинации, потребуется запустить 250 (1 125 899 906 842 624) тестов.

190

Глава 17

Это больше нониллиона тестов — вы можете выполнять по тесту в секунду до конца жизни, и вам все равно не хватит времени. Однако если вас устраивает проверка
всех двусторонних взаимодействий, мы можете снизить это число до 14 тестов! Эта
экономия на тестах воистину непостижима в числах процентов — речь идет о множестве порядков. То, что раньше казалось пугающим, теперь становится легко достижимым. Увеличение количества взаимодействий увеличивает количество тестов
незначительно: например, тестирование всех трехсторонних взаимодействий потребует сорока тестов, а тестирование всех четырехсторонних взаимодействий потребует всего сотню тестов. Более того, NIST ACTS сгенерировал соответствующие тест-планы на моем маломощном ноутбуке менее чем за несколько секунд.
Вы можете увидеть, что сублинейно растет не только количество необходимых тестов; чем больше у вас переменных, тем больший процент времени вы сэкономите
благодаря комбинаторному тестированию. В начале этой книги мы говорили о том,
что для многих программ исчерпывающее тестирование практически невозможно.
Применение комбинаторного тестирования является одним из способов облегчить
эту проблему. Используя небольшой процент усилий, необходимых для исчерпывающего тестирования, мы можем найти подавляющее большинство дефектов,
которые могли бы быть найдены во время его проведения.

ГЛАВА 18

Стохастическое тестирование
и тестирование на основе свойств
Термин "стохастический" происходит от греческого слова στοχαστικός, которое является формой στοχάζεσθαι, означающего "направляйся к" или "предполагай". Это
хорошая этимология для стохастического тестирования, использующего случайные процессы, которые могут быть проанализированы при помощи статистики, но
не предсказаны точно. На первый взгляд может показаться смешным использовать
случайность в тестировании ПО; в конце концов, разве фундаментальная концепция тестирования не заключается в определении ожидаемого поведения и проверке
того, что оно равняется наблюдаемому поведению? Если вы не знаете, что подается
на вход, как вы узнаете, какой должен быть ожидаемый выход?
Ответ заключается в том, что существуют ожидаемые поведения и свойства, про
которые вы знаете, что они имеются в системе, и которые не зависят от того, что вы
подаете на вход. Например, вне зависимости от того, какой код передается компилятору, вы ожидаете, что компилятор не выйдет из строя. Он может выдать сообщение о невозможности компиляции. Он может сгенерировать исполняемый файл.
Этот исполняемый файл может запуститься, а может и нет. Вы ожидаете, что в системе нет ошибки сегментации. Таким образом, вы по-прежнему можете запускать
тесты, где ожидаемым поведением для любых входных данных будет "система не
выходит из строя".
Предоставляя системе метод использования случайных данных в качестве входных
данных, вы также снижаете стоимость тестирования. Вам больше не нужно продумывать множество конкретных тест-кейсов, а затем тщательно записывать их или
программировать. Вместо этого вы просто связываете некий генератор случайных
чисел и некий способ генерирования тестовых данных на его основе, а ваш компьютер выполняет всю работу по генерации тест-кейсов. Несмотря на то что генератор случайных чисел может быть не так хорош, как специалист по тестированию
при разработке граничных случаев, разделении классов эквивалентности и т. п., он
часто обнаруживает множество проблем просто из-за способности сгенерировать
огромное количество тестов и быстро их выполнить.
Стохастическое тестирование часто называется обезьяньим тестированием
(monkey testing) по аналогии с обезьяной, стучащей по кнопкам клавиатуры. Однако "стохастическое тестирование" звучит гораздо более впечатляюще, особенно

192

Глава 18

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

18.1. Бесконечные обезьяны
и бесконечные пишущие машинки
Существует старая притча об обезьянах и пишущих машинках, которые при обычных условиях не взаимодействуют друг с другом. Притча гласит, что миллион
обезьян при наличии бесконечного времени однажды напишут работы Шекспира
(предполагается, что обезьяны в данном случае бессмертны). Стохастическое тестирование следует подобному принципу — большой набор случайных входных
данных (миллион обезьян, стучащих по кнопкам) и достаточно большой период
времени позволят обнаружить дефекты. В данной аналогии наш тестировщик является Уильямом Шекспиром (не принимайте это близко к сердцу). Тестировщик
определенно может написать работы Шекспира быстрее, чем миллион обезьян. Однако обезьяны (как и компьютерный генератор случайных чисел) гораздо дешевле,
чем реанимация зомби Уильяма Шекспира. Даже если генератор случайных чисел
не такой хороший автор тестов, как вы (или Шекспир), благодаря их большому количеству он неизбежно столкнется с многочисленными интересными граничными
случаями и, возможно, обнаружит дефекты.
Так как вы — а более точно, стохастическая система тестирования — можете не
знать, каким должно быть ожидаемое поведение для заданных входных данных,
вам необходимо проверять свойства системы. На уровне юнит-тестирования, где
вы проверяете отдельные методы, это называется тестированием на основе
свойств (property-based testing).

18.2. Тестирование на основе свойств
Давайте еще раз предположим, что мы тестируем нашу функцию сортировки
billSort. Как вы помните, предполагается, что она работает в 20 раз быстрее, чем
любой другой алгоритм сортировки. Однако существуют вопросы о правильности
ее работы, поэтому вам поручили проверить, как она работает во всех случаях.
Какой вид входных данных использовать при тестировании? Предположим, что
сигнатура метода выглядит так:
public int[] billSort(int[] arrToSort) {
...
}

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

194

Глава 18

3. Значение каждого последующего элемента в выходном массиве больше или
равно предыдущему значению.
4. Ни один из не присутствующих во входном массиве элементов также не находится в выходном массиве.
5. Функция идемпотентная; т. е. вне зависимости от того, сколько раз эта функция
вызывается, возвращаться должны те же выходные данные. Если я запущу метод
сортировки на списке, а затем снова запущу сортировку на полученных данных,
то второй вызов сортировки должен создать тот же выходной массив, что и после первого запуска. Обратите внимание, что было бы труднее тестировать
функцию, обратную идемпотентной (неидемпотентную функцию), т. к. в этом
случае выходные данные могут изменяться, а могут не изменяться (например,
предположим, что функция увеличивает значение и возвращает это значение
по модулю 6; каждый раз при вызове этой функции с множителем 6 она вернет
одно и то же значение).
6. Функция является чистой; если запустить ее дважды с одинаковым входным
массивом значений, то на выходе всегда должен быть одинаковый выходной
массив. Каждый раз, когда я вызываю сортировку для списка [3, 2, 1], всегда
будет возвращаться [1, 2, 3]. Неважно, будет ли луна в фазе роста или убывания, или какие установлены значения глобальных переменных, для одного и
того же входного массива будет возвращаться одинаковое выходное значение.
Теперь, когда у нас есть некоторые свойства, которые мы ожидаем от любого выхода метода billSort(), мы может позволить компьютеру выполнять основную работу
по созданию массивов данных, передаче их в наш метод и последующей проверке,
соответствует ли полученный выходной массив всем тем свойствам, что мы установили. Если выходной массив не соответствует одному из инвариантов, мы сообщаем об ошибке тестировщику. Генерация выходных данных, которые не соответствуют определенному инварианту, называется фальсификацией инварианта.
Существует множество библиотек для Java, которые выполняют тестирование на
основе свойств, но стандарта такого тестирования нет. Тестирование на основе
свойств гораздо более популярно в мире функционального программирования,
причем такие программы, как QuickCheck для Haskell, используются чаще, чем
стандартные юнит-тесты. Фактически концепция автоматизированного тестирования на основе свойств исходит из функционального мира и сообщества Haskell в
частности. Для получения дополнительной информации обратитесь к "QuickCheck:
Легкий инструмент для случайного тестирования программ на Haskell" Коэна
Классена и Джона Хьюза (Koen Claessen, John Hughes "QuickCheck: A Lightweight
Tool for Random Testing of Haskell Programs").

18.3. Умные, тупые, злые
и хаотические обезьяны
Как было упомянуто выше, стохастическое тестирование часто называется обезьяньим тестированием. Но не так хорошо известно, что существует несколько видов
обезьян, которые могут выполнять работу по тестированию за нас.

196

Глава 18

чем:
> jiwh0t34h803h8t32h8t3h8t23
ERROR
> aaaaaaaaaaaaa
ERROR
> 084_==wjw2933
ERROR

Хотя это предположение может не выполняться, если программой займутся малыши, в целом оно, скорее всего, верно. Таким образом, чтобы сосредоточить наши
ресурсы тестирования на выявлении дефектов, мы можем использовать умное
обезьянье тестирование, чтобы действовать как пользователь. Однако поскольку
данное тестирование автоматизируется, оно сможет работать гораздо быстрее и на
гораздо большем количестве возможных входных данных, чем выполняемое пользователем ручное тестирование.
Создание теста умной обезьяны может быть сложным, потому что вам нужно не
только понимать, как именно пользователи будут работать с приложением, но и
также разработать модель и генератор для этого. Однако то, что умная обезьяна
найдет дефект в тестируемой системе, является более вероятным.
Злое обезьянье тестирование симулирует поведение злонамеренного пользователя, который активно пытается повредить вашу систему. Это может быть реализовано через отправку чрезвычайно длинных строк, возможные инъекционные атаки,
неправильно сформированные данные, неподдерживаемые символы или прочие
входные данные, которые задумывались с целью вызвать разрушения в вашей системе. В современном связанном сетями мире системы почти всегда находятся под
атакой, даже если они подключились к Интернету всего на несколько миллисекунд.
Гораздо лучше иметь в наличии злую обезьяну, определяющую, уязвима ли система, нежели ждать, когда за нее это сделает реальный злоумышленник!
Давайте предположим, что мы сохраняем все вводимые данные нашей арифметической программы в базе данных. Тест злой обезьяны может проверить, способна ли
программа каким-то образом перезаписать или изменить эти данные путем передачи ей вредоносного запроса SQL или неких символов, которые могут быть интерпретированы как пустые (null), или слишком длинной строки, способной вызвать
переполнение буфера.
> '); DELETE FROM entries; --"
ERROR
> \000\000\000
ERROR
> Eh bien, mon prince. Gênes et Lucques ne sont plus que des apanages, des поместья,
de la famille Buonaparte...
ERROR

По последней строке можно предположить, что злой обезьяной был фактически
введен весь текст "Войны и мира" Льва Толстого. Я хотел добавить его, чтобы увеличить количество слов в моей книге, но решил отказаться от этой идеи, чтобы

198

Глава 18

полностью доброкачественной; скажем, она изменила значение по умолчанию переменной, которое затем сразу же было перезаписано. Также существует вероятность, что модификация внесла изменения, которые при работе окажутся не видны.
Давайте проработаем пример мутационного тестирования. Предположим, что у нас
есть метод, который на основе длины шеи животного пытается определить вид этого животного.
public class Guess {
public static String animalType (int neckLength) {
String toReturn = "UNKNOWN";
if (neckLength < 10) {
toReturn = "Rhinoceros";
} else if (neckLength < 20) {
toReturn = "Hippopotamus";
} else {
toReturn = "Giraffe";
}
return toReturn;
}
}

Наши тест-кейсы проверяют по одному значению из каждого класса эквивалентности.
@Test
public void animalTypeRhinoceros() {
assertEquals("Rhinoceros", Guess.animalType(5);
}
@Test
public void animalTypeHippopotamus() {
assertEquals("Hippopotamus", Guess.animalType(15);
}
@Test
public void animalTypeGiraffe() {
assertEquals("Giraffe", Guess.animalType(25);
}

Сейчас, если вы помните нашу главу о юнит-тестировании, это не очень хорошее
покрытие. Конечно, это не самое худшее юнит-тестирование метода, которое я
видел. Мутационное покрытие может дать нам лучшее представление об общем
качестве наших тестов, чем простая оценка с точки зрения покрытия кода. Давайте
рассмотрим несколько мутаций, которые вызовут сбой, показывая нам сильные
стороны юнит-тестов и то, как можно заставить их завершиться неуспешно.
public class Guess {
public static String animalType(int neckLength) {
String toReturn = "UNKNOWN";
// Значение изменено с 10 на 1
if (neckLength < 1) {
toReturn = "Rhinoceros";

200

Глава 18
} else if (neckLength < 20) {
toReturn = "Hippopotamus";
} else {
toReturn = "Giraffe";
}
return toReturn;
}

}

В этом случае все наши юнит-тесты пройдут: животное с длиной шеи, равной 5,
будет идентифицировано как носорог (rhinoceros), с длиной шеи, равной 15, — как
бегемот (hippopotamus), а с длиной шеи, равной 25, — как жираф (giraffe). Но, к
сожалению, функциональность метода определенно изменилась! Если кто-то вызовет этот мутированный метод, используя в качестве аргумента neckLength, равный 9,
животное будет определено, как бегемот, хотя это носорог. Это показывает нам
важность проверки граничных значений между классами эквивалентности. Если бы
мы проверили все эти границы, тогда один из наших юнит-тестов завершился бы
сбоем при изменении этих значений.
Как мы говорили, некоторые мутации могут не вызывать сбоев тестов, но при этом
быть доброкачественными. Зачастую это означает, что измененный код является
излишним или что он реализован неправильно, но на его выполнение это не оказывает влияния.
public class Guess {
public static String animalType(int neckLength) {
String toReturn = "AMBER";
// Значение строки изменено с "UNKNOWN" на "AMBER"
if (neckLength < 10) {
toReturn = "Rhinoceros";
} else if (neckLength < 20) {
toReturn = "Hippopotamus";
} else {
toReturn = "Giraffe";
}
return toReturn;
}
}

Это не вызовет никаких сбоев, т. к. значение "UNKNOWN" было просто заполнителем и
ни для чего не использовалось. Это говорит нам о том, что, возможно, нам не требуется значение по умолчанию или что существует лучший способ реализации
метода. Например, немного более эффективный способ реализации того же метода:
public class Guess {
public static String animalType(int neckLength) {
String toReturn = "Giraffe";
// Значение по умолчанию теперь Giraffe, и не надо использовать
// else для завершения

Стохастическое тестирование и тестирование на основе свойств

201

if (neckLength < 10) {
toReturn = "Rhinoceros";
} else if (neckLength < 20) {
toReturn = "Hippopotamus";
}
return toReturn;
}
}

Тем не менее в этом коде мы забыли реализовать некий функционал. Возможно, мы
намеревались возвращать это значение по умолчанию, если передано отрицательное значение длины шеи, но не удосужились прописать это в коде!
public class Guess {
public static String animalType(int neckLength) {
String toReturn = "UNKNOWN ";
// Если передано неверное значение neckLength, возвращаем
// значение по умолчанию
if (neckLength < 0) {
return toReturn;
}
if (neckLength < 10) {
toReturn = "Rhinoceros";
} else if (neckLength < 20) {
toReturn = "Hippopotamus";
} else {
toReturn = "Giraffe";
}
return toReturn;
}
}

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

ГЛАВА 19

Тестирование производительности
А что такое тестирование производительности? Перед тем как мы определим, что
такое "тестирование производительности", нам необходимо определить, что такое
"производительность". Я полагаю, что к этому разделу книги все уже понимают,
что означает "тестирование".
Большинство технически подкованных людей будет иметь представление о том,
что означает производительность — системы являются "быстрыми", они "не занимают много памяти" и т. д. Впрочем, чем дольше вы пытаетесь сформулировать,
тем более расплывчатым становится определение. Быстрые по сравнению с чем?
Что, если система занимает много оперативной памяти, а конкурирующая система
занимает такой памяти меньше, но для нее требуется больше места на жестком
диске? Если система A возвращает ответ за три секунды, а система B за пять секунд, какая из них более производительная? Ответ может показаться очевидным,
если только запросы, на которые отвечают системы, сами по себе не различаются.
Производительность — это одна из тех концепций, которую сложно определить,
потому что она будет означать разное для различных систем.
Представим видеоигру, где вы играете за тестировщика, сражающегося со злыми
багами. Вы можете использовать клавиши-стрелки для движения и пробел для
стрельбы из вашей инсектицидной винтовки. Каждый раз, когда вы нажимаете клавишу пробела, вы предполагаете, что ваш экранный персонаж начнет стрелять инсектицидами, скажем, в течение 200 мс — если это время окажется больше, вам
покажется, что игра "лагает" после нажатия клавиш. Теперь представим вторую
систему — прогнозирующий погоду суперкомпьютерный кластер, способный одновременно вести миллионы расчетов. Но из-за того, что прогноз погоды требует
множества расчетов, требуется полчаса, чтобы после нажатия кнопки запуска получить прогноз на завтра. А до этого момента на экране будет отображаться "Ведется расчет...". У суперкомпьютера время отклика на несколько порядков больше,
чем у видеоигры, но означает ли это, что суперкомпьютер менее производительный, чем игровая приставка? Можно поспорить о таком показателе производительности, как время отклика, но результаты времени отклика для погодного компьютера оказались значительно отличающимися.
Как определить, у какой системы лучшая производительность? Короткий ответ —
никак! Системы выполняют различные — и в основном несовместимые — дейст-

Тестирование производительности

203

вия. То, как вы измеряете производительность, будет зависеть от типа тестируемой
системы. Впрочем, это не означает, что не существует правил или эвристики.
В этой главе мы рассмотрим некоторые различные виды производительности и
способы их тестирования. Однако это далеко не исчерпывающий список. У тестируемой вами системы могут быть уникальные аспекты производительности, которые не будут описаны ни в одной из книг конечной длины (которой, несомненно,
является и эта книга).

19.1. Категории
показателей производительности
Как было упомянуто выше, определение производительности для конкретной системы означает определение того, какие показатели производительности важны для
пользователя или потребителя. Хотя существует огромное количество всевозможных показателей производительности, наиболее важными являются две основные
категории: ориентированные на сервис и ориентированные на эффективность.
Показатели, ориентированные на сервис, измеряют, насколько хорошо система
предоставляет услугу конкретному пользователю. Они измеряются с точки зрения
пользователя и количественно определяют аспекты системы, которые важны для
пользователя. Примерами ориентированных на сервис показателей могут быть
среднее время загрузки страницы, время отклика или процент времени, в течение
которого доступна система.
Существуют две основные подкатегории показателей, ориентированных на сервис.
Первой является доступность — насколько доступна система для пользователя?
Здесь может подразумеваться все, что угодно, от процентов времени работоспособности системы до того, насколько различные функции доступны пользователям
в различное время. Второй основной подкатегорией является время отклика — как
долго система отвечает на действия пользователя или возвращает результат?
Показатель, ориентированный на эффективность, измеряет, насколько эффективно система использует доступные вычислительные ресурсы. Вы можете смотреть на них как на показатели "с точки зрения программы" или по крайней мере
с точки зрения компьютера. Примерами ориентированных на эффективность показателей могут быть загрузка процессора при выполнении определенного функционала, объем используемого дискового пространства или количество пользователей,
которых одновременно может обслуживать система на конкретном сервере. Хотя
последний пример может показаться ориентированным на сервис, то, что он связан
с системным уровнем — сколько пользователей может обслуживать система, а не
как это выглядит для определенного пользователя, — означает, что это показатель,
ориентированный на эффективность. Каждый раз, когда вы измеряете с точки зрения системы, а не с позиций того, с чем напрямую имеет дело пользователь, вы тестируете ориентированный на эффективность показатель.
Как и в показателях, ориентированных на сервис, здесь имеются две основные подкатегории ориентированного на эффективность тестирования. Пропускная спо-

204

Глава 19

собность измеряет количество событий, которые может обработать система за определенный промежуток времени, например сколько пользователей могут авторизоваться одновременно или сколько пакетов может маршрутизировать система за
30 секунд. Утилизация измеряет процент или абсолютное значение компьютерных
ресурсов, которые используются для выполнения заданной операции. Например,
сколько дискового пространства требуется базе данных для хранения определенной
таблицы? Сколько оперативной памяти потребуется для ее сортировки?
Показатели, ориентированные на сервис, подобны тестированию черного ящика
в том, что они измеряют систему со стороны, без точного определения причины
проблемы. Как прямолинейные инструменты они в целом эффективны в поиске
того, где следует присмотреться к проблемам. Показатели, ориентированные на
эффективность, подобны тестированию белого ящика, т. к. они требуют как понимания системы, так и общих технических знаний. Зачастую вы можете использовать ориентированные на сервис показатели для определения областей, где производительность может оказаться проблемой для пользователей, а затем использовать
ориентированные на эффективность показатели для точного определения того, что
вызвало проблему. Длительное время отклика с точки зрения пользователя может
быть результатом задействования физической оперативной памяти и необходимости обмена большой части информации с диском. Ориентированное на сервис тестирование может обнаружить первую проблему, а ориентированное на эффективность тестирование сможет выяснить вторую.

19.2. Тестирование производительности:
пределы и цели
Для определения, прошел ли успешно тест производительности или нет, вам необходимы цели производительности, или определенные числовые значения, которых предположительно должны достигать показатели производительности. Например, у вас может быть ориентированная на эффективность цель производительности, согласно которой инсталлятор тестируемой программы должен быть меньше
10 Мбайт (в наше время программа "Hello, world!" может занимать больше десяти
мегабайт, но давайте не пока не будем жаловаться). У вас может быть ориентированный на сервис показатель, согласно которому система при нормальной нагрузке
должна отвечать в течение 500 миллисекунд. Приводя цель к численному показателю, вы можете написать тест и определить, соответствует ли этому показателю система или нет.
Впрочем, цели являются идеалом. Зачастую, конкретный показатель производительности системы не может достигнуть своей цели. Порог производительности
показывает точку, при которой показатель производительности достигает абсолютно минимальную приемлемую производительность. Например, хотя время отклика
системы может составлять 500 миллисекунд, системные инженеры определили, что
система приемлема для работы при времени отклика 3000 миллисекунд. Не очень

206

Глава 19

определять их через пользовательское тестирование, изучая аналогичные приложения в данной области, или использовать для оценки свои знания по тестированию.
Выбор ключевых показателей производительности должен быть осуществлен перед
началом тестирования; вы не должны тестировать множество показателей, а затем
осмотреться и понять, какие из них важны. Вам нужно определить самые важные
из них заранее, при разработке тест-плана для системы. Если вы следуете более упрощенному процессу разработки с менее значимой предварительной проработкой,
вы должны, по крайней мере, определить, является ли конкретный показатель производительности ключевым перед запуском соответствующего теста. Это заставит
вас помнить, какие аспекты производительности системы важны, а какие — нет,
прежде, чем вы получите результаты. Если вы попытаетесь выбрать KPI позже,
у вас может возникнуть соблазн (даже подсознательно) просто выбрать те показатели, которые соответствуют целям. Оставьте такое мышление рекламодателям.

19.4. Тестирование показателей,
ориентированных на сервис: время отклика
Самый простой способ измерить время отклика, конечно же, просто следовать данному алгоритму:
1. Сделать что-то.
2. Нажать кнопку "Старт" на секундомере.
3. Ждать отклика.
4. Когда появляется отклик, нажать кнопку "Стоп" на секундомере.
5. Записать, сколько прошло времени.
Хотя это может быть простейший способ измерить время отклика, он далеко не
лучший. У данного подхода переизбыток проблем. Во-первых, невозможно измерить доли секунды; время реакции человека слишком переменное и недостаточно
быстрое. Вы не можете измерить нечто внутри системы, если вашим единственным
интерфейсом системы является то, что вы видите. Такая работа занимает очень
много времени, что затрудняет сбор больших наборов данных. Здесь возможна человеческая ошибка. (Вы когда-нибудь тратили целый день на измерения чего-либо?
В какой-то момент вы обязательно забудете нажать кнопку "Старт" или случайно
нажмете кнопку "Сброс" до того, как вы записали время.) Это идеальный способ
испортить настроение тестировщику. (Вы когда-нибудь тратили целый день на измерения чего-либо? В какой-то момент вы начнете задумываться о поиске другой
работы.) Из-за всех этих (и других) проблем, тестирование производительности
часто осуществляется при помощи специальных программ.
Хотя тестирование в целом все больше и больше автоматизируется, конкретно тестирование производительности значительно зависит от автоматизированных тестов. Показатели производительности могут значительно отличаться от запуска
к запуску из-за влияния различных переменных, которые вы можете практически

208

Глава 19

уменьшить количество выполняемых программой системных вызовов, изменить их
порядок, вызывать другие функции или использовать прочие способы уменьшения
системного времени.
Суммируя пользовательское время и системное время, вы получите общее время
(total time) — количество времени, потраченного на выполнение кода в пользовательском пространстве или пространстве ядра. Это хороший показатель того,
сколько времени тратится на выполнение вашего кода. Это позволяет избежать
расчета времени, в течение которого другие процессы выполнялись процессором
или система ожидала ввода, и дает возможность просто сосредоточиться на том,
как долго выполнялся код. Тем не менее чрезмерное внимание к системному времени может отвлечь вас от проблем, связанных с соответствующими внешними
факторами. Например, если вы тестируете работающую с базой данных программу,
то бóльшая часть потраченного времени будет связана с чтением диска и записью
на него. Было бы глупо сбрасывать со счетов это время — даже если у разработчиков нет прямого контроля над ним, его все равно следует учитывать.
В зависимости от того, что вы измеряете и какой контроль у вас над тестовой средой, отслеживание пользовательского, общего или реального времени может быть
оптимальным показателем. Системное время используется редко, если только вы не
тестируете новое ядро, операционную систему или сильно беспокоитесь о них по
непонятным причинам. Помните, что хотя разработчики могут большей частью
контролировать пользовательское время, опосредованно общее время и в большинстве случаев минимально контролируют реальное время, пользователей обычно
волнует только реальное время! Если пользователь системы посчитает ее медленной, его не заинтересуют объяснения, что вы не можете контролировать скорость
чтения с диска. С точки зрения тестирования, вам следует сфокусироваться на
измерении реального времени, избегая при возможности посторонних факторов,
таких как одновременный запуск других программ. Тем не менее для определенных
процессов, которые связаны с центральным процессором или обычно работают
в фоновом режиме, более подходящим будет фокусирование на общем или пользовательском времени.
В большинстве UNIX-подобных систем (таких как Linux или OS X) вы можете
очень легко получить эти значения для оценки программы. Просто запустите
команду time , и после завершения программы будет отображено реальное,
пользовательское и системное время (общее время легко вычисляется путем сложения пользовательского и системного времени):
$ time wc -w * | tail -n 1
66156 total
real
0m0.028s
user
0m0.009s
sys
0m0.014s
$

Для Windows есть подобные разнообразные программы, например timeit.exe, которая поставлялась с Windows Server 2003 Resource Kit, но по умолчанию в состав
операционной системы такие утилиты не входят.

210

Глава 19

лом разработки, поэтому часто тестировщику приходится обращать внимание команды на проблемы, даже если перед ним не стояла задача заниматься подобными
вопросами.
Определение допустимых границ времени отклика может быть трудным. Тем не
менее существуют некоторые приблизительные рекомендации. Приведенные ниже
взяты из книги "Инжиниринг юзабилити" Джейкоба Нильсена (Jakob Neilsen
"Usability Engineering").