Понятие и виды тестирования.
Тестирование – это проверка соответствия между реальным поведением программы и ее ожидаемым поведением в специально заданных, искусственных условиях. Разберем это определение по частям.
Ожидаемое поведение программы. Исходной информацией для тестирования является знание о том, как система должна себя вести, то есть требования к ней или к ее отдельной части. Самым распространенным способом тестирования является тестирование методом черного ящика, то есть когда реализация системы недоступна тестировщикам, а тестируется только ее интерфейс. Часто это закрепляется и организацией коллектива – тестировщики оказываются отдельными сотрудниками и в некоторых компаниях они даже принципиально не общаются с разработчиками, чтобы минимально знать реализационных деталей и максимально полно выступить в роли проверяющей инстанции. Существует тестирование методом белого ящика, когда код программ доступен тестировщикам и используется в качестве источника информации о системе2. Его схема представлена на рис. 7.1.
Рис. 7.1.
На этом рисунке видно, что на основе требований к системе создается реализация и тестовая модель системы. Тестирование есть сопоставление двух этих представлений с целью выявить их несоответствия. Чем независимее друг от друга будут эти представления, тем больше прока от их сопоставления. Иначе, если тестировщики существенно используют информацию о реализации системы при составлении тестов, то они могут невольно внести в тесты ошибки реализации. Найденное при тестировании несоответствие – это еще не ошибка, поскольку сами тестировщики могли неправильно понять требования, в тестах и средствах тестирования могли быть ошибки.
Данный подход закрепляется также и в организации коллективов программистов - тестировщики, как правило, отделены от разработчиков. Это разные люди, несовместимые роли в MSF. Авторы слышали рассказ об одной американской компании где разработчики и тестировщики сидели на разных этажах, ходили в разной одежде (тестировщики в костюмах, разработчики – в свитерах) и начальство не поощряло нерабочие отношения между этими группами. Это, конечно же, крайность, но она еще раз подчеркивает, как важно, чтобы точка зрения на систему у тестеров отличалась от точки зрения разработчиков. Но, конечно, и та и другая должны исходить из общего видения системы – ее требований.
Специально заданные, искусственные условия, – те условия, где осуществляется тестирование. При этом ключевым аспектом здесь является наличие тестов – воспроизводимых шагов манипуляции с системой, приводящих к ее некорректной работе. Концепция теста очень важна, так как необходимо не просто обнаружить некорректное поведение системы, а создать и зафиксировать алгоритм воспроизведения ошибки – чтобы повторить его для разработчика или чтобы разработчик сам смог воспроизвести ошибку. Если ошибка не воспроизводится, то нет возможности ее исправить.
Тесты могут быть "ручными" и автоматизированными. "Ручной" тест – это последовательность действий тестировщика, которую он (или разработчик) может воспроизвести и ошибка произойдет. Как правило, в средствах контроля ошибками такие последовательности действий содержатся в описании ошибки. Автоматический тест – это некоторая программа, которая воздействует на систему и проверяет то или иное ее свойство. Автоматический тест, по сравнению с "ручным", можно легко воспроизводить без участия человека. Можно создавать наборы тестов и прогонять их часто, например, в режиме регрессионного тестирования. Кроме того, автоматические тесты можно генерировать по более высокоуровневым спецификациям, например, по формально описанным требованиям к системе. А, например, тесты для компиляторов можно генерировать по формальному описанию языка программирования.
Таким образом, преимущества автоматических тестов перед "ручными" очевидны. Поговорим теперь о трудностях автоматического тестирования.
Во-первых, для того, чтобы тесты автоматически запускать, нужны соответствующие программные продукты, которые также являются неотъемлемой частью специально заданных, искусственных условий, которые мы сейчас обсуждаем. Их будем называть инструментами тестирования. В их задачу входит запуск теста на системе, "прогон" целого пакета тестов, а также анализ получившихся результатов и их обработка.
Кроме того, немаловажной задачей инструментов тестирования является обеспечение доступа теста к системе через некоторый ее интерфейс. Доступ к системе может оказаться затруднительным, например, в силу политических обстоятельств, когда сторонними разработчиками делается подсистема некоторой стратегической системы, и доступ к этой объемлющей системе у разработчиков сильно ограничен. Или в силу аппаратных ограничений – трудно "залезть" на "железку", где работает целевой код системы.
Кроме того, часто трудно "бесшовно" тестировать систему, оказывая на нее минимальное воздействие и добираясь при этом до всех аспектов ее функционирования. В целом, настройка и развертка готовых, сторонних тестовых инструментов часто оказывается дорогостоящей и непростой задачей. Разработка своих собственных тестовых инструментов также непроста.
Во-вторых, часто возникает проблема ресурсов для автоматического тестирования. Особенно при автоматической генерации тестов: часто есть возможность автоматически сгенерировать очень большое количество тестов, так что если их еще выполнять регулярно, в режиме непрерывной интеграции, то не хватит имеющихся системных ресурсов. При этом качество тестирования может оказаться неудовлетворительным – ошибки находятся редко или вообще не находятся. Дело в том, что количество всех возможных состояний программной системы очень велико, и тестирование не может покрыть их все. На практике, в реальных проектах, определяют критерии тестирования, которые определяют ту "планку" качества, которую необходимо достичь в этом проекте. Ведь хорошее качество стоит дорого и очевидно, что разное ПО имеет разное качество, например, система управления ядерным реактором и текстовый редактор. На практике, часто, качество ПО определяется бюджетом проекта по его разработке. Далее, в силу ограниченности ресурсов на тестирование часто целесообразно бывает определить те аспекты ПО, которые наиболее важны -как для общей работоспособности системы, так и для заказчика. Например, при тестировании Web-приложения, предоставляющего услугу по созданию объявлений о продаже недвижимости, такими критериями были:
· правильность переходов сложного мастера – в частности, в связи с возможностью переходов назад;
· целостность введенных пользователем данных о создаваемых объявлениях.
Наконец, кроме ограничения количества тестов их отбора важным является их прогон на некоторых (не на всех возможных!) входных данных. Часто здесь применяют принцип факторизации – множество всех возможных входных значений разбивают на значимые с точки зрения тестирования классы и "прогоняют" тесты не на всех возможных входных значениях, а берут по одному набору значений из каждого класса. Например, тестируют некоторую функцию системы на ее граничные значения – очень большие значения параметров, очень маленькие и пр. Часто факторизацию удобно делать, исходя из требований к данной функции, также бывает полезно посмотреть на ее реализацию и "пройтись" тестами по разным ее логическим веткам (порождаемым, например, условными операторами).
Виды тестирования. Не претендуя на полноту, выделим следующие виды тестирования.
· Модульное тестирование - тестируется отдельный модуль, в отрыве от остальной системы. Самый распространенный случай применения – тестирования модуля самим разработчиком, проверка того, что отдельные модули, классы, методы делают действительно то, что от них ожидается. Различные среды разработки широко поддерживают средства модульного тестирования – например, популярная свободно распространяемая библиотека для Visual Studio NUnit, JUnit для Java и т.д. Созданные разработчиком модульные тесты часто включаются в пакет регрессионных тестов и таким образом, могут запускаться многократно.
· Интеграционное тестирование – два и более компонентов тестируются на совместимость. Это очень важный вид тестирования, поскольку разные компоненты могут создаваться разными людьми, в разное время, на разных технологиях. Этот вид тестирования, безусловно, должен применяться самими программистами, чтобы, как минимум, удостовериться, что все живет вместе в первом приближении. Далее тонкости интеграции могут исследовать тестровщики. Необходимо отметить, что такого рода ошибки – "ошибки на стыках" - непросто обнаруживать и устранять. Во время разработки все компоненты все вместе не готовы, интеграция откладывается, а в конце обнаруживаются трудные ошибки (в том смысле, что их устранение требует существенной работы). Здесь выходом является ранняя интеграция системы и в дальнейшем использование практики постоянной интеграции.
· Системное тестирование – это тестирование всей системы в целом, как правило, через ее пользовательский интерфейс. При этом тестировщики, менеджеры и разработчики акцентируются на том, как ПО выглядит и работает в целом, удобно ли оно, удовлетворяет ли она ожиданиям заказчика. При этом могут открываться различные дефекты, такие как неудобство в использовании тех или иных функций, забытые или "скудно" понятые требования.
· Регрессионное тестирование – тестирование системы в процессе ее разработки и сопровождение на регресс. То есть проверяется, что изменения системы не ухудшили уже существующей функциональности. Для этого создаются пакеты регрессионных тестов, которые запускаются с определенной периодичностью – например, в пакетном режиме, связанные с процедурой постоянной интеграции.
· Нагрузочное тестирование – тестирование системы на корректную работу с большими объемами данных. Например, проверка баз данных на корректную обработку большого (предельного) объема записей, исследование поведение серверного ПО при большом количестве клиентских соединений, эксперименты с предельным трафиком для сетевых и телекоммуникационных систем, одновременное открытие большого числа файлов, проектов и т.д.
· Стрессовое тестирование – тестирование системы на устойчивость к непредвиденным ситуациям. Этот вид тестирования нужен далеко не для каждой системы, так как подразумевает высокую планку качества.
· Приемочное тестирование – тестирование, выполняемое при приемке системы заказчиков. Более того, различные стандарты часто включают в себя наборы приемочных тестов. Например, существует большой пакет тестов, поддерживаемых компанией Sun Microsystems, которые обязательны для прогона для всех новых реализаций Java-машины. Считается, что только после того, как все эти тесты успешно проходят, новая реализация вправе называться Java.
Работа с ошибками.
Между программистами и тестировщиками необходим специальный интерфейс общения. Ведь ошибок находится много, их исправление требует времени, и их исправления разработчиками тестировщики должны удостовериться, что они действительно исправлены. Кроме того, менеджерам нужна статистика по найденным и исправленным ошибкам – это хороший инструмент контроля проекта. Все это изображено на рис. 7.2. Чтобы справиться с этим потоком информации и обеспечить необходимые в работе, удобные сервисы, существует специальный класс программных средств – средства контроля ошибок (bug tracking systems).
Рис. 7.2.
Как правило, описание ошибки в системе контроля ошибок имеет следующие основные атрибуты:
· ответственного за ее проверку – тестировщика, который ее нашел и который проверяет, что исправления, сделанные разработчиком, действительно устраняют ошибку;
· ответственного за ее исправление – разработчика, которому ошибка отправляется на исправление;
· состояние, например, ошибка найдена, ошибка исправлена, ошибка закрыта, ошибка вновь проявилась и т.д.
Этот список существенно дополняется в различных программных средствах контроля ошибок, но это основные атрибуты.
Использование этих систем давно стало общей практикой в разработке ПО, наравне со средствами версионного контроля и многими иными инструментами. Они включают в себя:
· базу данных для хранения ошибок;
· интерфейс к этой базе данных для внесения новых ошибок и задания их многочисленных атрибутов, для просмотра ошибок на основе различных фильтров – например, все найденные ошибки за последний месяц, все ошибки, за которые отвечает данный разработчик и т.д.;
· сетевой доступ, так как проекты все чаще оказываются распределенными;
· программный интерфейс для возможностей программной интеграции таких систем с другим ПО, поддерживающим разработку ПО (например, со средствами непрерывной интеграции – они могут автоматически вносить в базу данных найденные при автоматическом прогоне тестов ошибки).
Очень важным при работе с ошибками оказываются различные отчеты, о чем будет подробно рассказано при обсуждении VSTS.
Метод случаи использования.
Описание примера. В качестве примера рассмотрим "Телефонную службу приема заявок". Заказчиком данной системы является компания, владеющая сетью продуктовых магазинов. Эта компания, кроме обычной розничной торговли и оптовых поставок продуктов отдельным столовым и ресторанам, хочет предоставлять еще и сервис по обслуживанию клиентов по телефонным заявкам. Клиент регистрируется в компании, а потом по телефону, в удобное для себя время, делает заказ товаров, которые к нему привозят домой, и он расплачивается. Для этого компания хочет организовать у себя локальный телефонный центр, состоящий из офисной многоканальной АТС, штата операторов и соответствующего программного обеспечения. При этом в компании уже есть информационная система по обработке заявок от постоянных мелкооптовых клиентов, и заказываемая система должна быть с ней проинтегрирована.
Работа с требованиями. Случаи или варианты использования (use cases) были предложены в конце 90-х годов Айвером Якобсоном, одним из главных авторов языка UML, как диаграммный подход для извлечения и первичной формализации требований к системам. Выше уже говорилось о сложности по формированию единой и связной картины требований к ПО. Необходимо извлечь требования из всех возможных источников, формализовать в некотором виде и обсудить. Этот процесс – извлечение, формализация, обсуждение – итеративен, то есть все делается не за один присест. Более того, сам способ формализации должен быть удобен для обсуждения, и в первую очередь, с потенциальными пользователями системы, которые могут быть совершенно не компетентны в IT. Их комментарии, одобрения и несогласия часто являются основой итеративного извлечения требований к системе. Кроме того, этот способ работы с информацией должен вести к созданию моделей, удобных в дальнейшей реализации системы. Другими словами, ясно формулировать исходные задачи для разработки. То есть способ формализации должен быть прост, понятен и обладать достаточной строгостью. Этим требованиям удовлетворяют диаграммы случаев использования, являющиеся на сегодняшний день составной частью стандарта UML.
Пример диаграммы случаев использования представлен на рис. 8.1.
Рис. 8.1.Пример диаграммы случаев использования
Итак, все начинается с точной идентификации пользователей будущей системы. Это – основа хороших требований и хорошей системы, ведь основная задача системы – удовлетворять потребности будущих пользователей. Для этого нужно их знать в лицо….. В нашем случае пользователями системы являются оператор, менеджер и представители технической поддержки и администрирования. Система должна также поддерживать внешний интерфейс с системой обработки заявок. Это — четвертый пользователь. Еще одним пользователем системы является Петров А.Б. — директор департамента сбыта товаров, который хочет периодически отслеживать деятельность телефонной службы приема заявок. Для него создано специальное пользовательское место с экранными формами статистики.
Различные пользователи ПО, изображаемые на диаграммах случаев использования, называются актерами (actors). Актеры могут обозначать:
· типовых пользователей ("Менеджер", "Оператор", "Техническая поддержка") — работников компании, сгруппированных по исполняемым обязанностям;
· другие системы, взаимодействующие с данной ("Система обработки заявок");
· выделенного пользователя ("Петров А.Б.").
Отметим, что выделенный пользователь существенно отличается от типового пользователя. Он, как правило, Важная Персона, и согласование функциональности для него согласуется лично с ним. Часто он влияет на оплату проекта, от его мнения о системе, во многом, зависит ее успешная сдача. Такие персоны, ради успеха проекта, нужно уметь идентифицировать и в рамках всей системы создавать некоторую функциональность специально для них и очень при этом стараться!
После идентификации пользователей происходит определение случаев использования ими системы. Прежде всего, определяется та функциональность системы, которая непосредственно помогает пользователям выполнять их работу, не связанную непосредственно с эксплуатацией системы. В нашем случае, для оператора важным плюсом от использования системы оказывается возможность получать быстрый доступ к справочной информации о клиентах, а также оперативно обрабатывать поступившие по телефону запросы на покупки (список товаров, цены, оформление заказа и пр.). Для менеджера важным является возможность оперативного просмотра текущих заявок (выполненных, в работе, отложенных, за определенный период времени и пр.), а также учет контроль рабочего времени операторов – кто и сколько времени потратил на разного вида работы (телефонные разговоры с клиентами, оформление заявки после окончания разговора и т.д.). При этом важно отметить, что функция учета рабочего времени может потребовать определенных действий со стороны операторов – например, нажимать соответствующую клавишу, уходя на обед или на перекур. Однако мы не обозначили соответствующую связь с этим случаем использования со стороны оператора, поскольку эта функциональность не помогает ему в непосредственной работе, а помогает его начальнику. Кроме того, мы не включили в случаи использования ряд сервисов, связанных с эксплуатацией системы, например, функцию логина в систему. Наличие четкой точки зрения при составлении диаграмм – залог их полезности.
Итак, случай использования (use case) — это независимая часть функциональности системы, обладающая результирующей ценностью для ее пользователей.
"Независимость" означает, что если случай использования всегда исполняется вместе с некоторым другим, то, по всей видимости, один из них нужно включить в другой (какой именно в какой, как назвать получившийся в итоге случай использования — зависит от обстоятельств).
"Результирующая ценность" случая использования для актера системы подразумевает, что он, данный случай использования, должен приносить актеру некоторый законченный и ценный с точки зрения его бизнеса результат. Будучи реализован системой, этот случай использования действительно делает бизнес актера эффективнее, производительнее. Тем самым разработка системы фокусируется на бизнес-целях, а незначительные случаи использования игнорируются, что важно для компактности модели. Ведь строится не абстрактная модель функций системы, а набор самых важных (для заказчика и пользователей) сервисов, чтобы каждый из них правильно понять и не один не упустить. И в дальнейшем контроль разработки системы будет осуществляться именно в терминах этого самого важного — того, что нужно заказчику и пользователям.
Случаи использования, соответствующие актерам "Техническая поддержка и администрирование" и "Служба обработки заявок" несколько не вписываются в представленное выше определение. Прежде всего, сами эти актеры не являются пользователями ПО, участвующими в основном бизнес-процессе обработки телефонных заявок. "Техническая поддержка и администрирование" занята поддержкой ПО и оборудования системы обслуживания телефонных заявок, а также ее администрированием (добавлением новых пользователей, назначением им соответствующих прав и пр.). "Служба обработки заявок" является уже существующей в компании информационной системой, имеющей базу данных и ряд сервисов по обработке заявок. Идентификация этих актеров и соответствующих им случаев использования важна с точки зрения определения требований к системе. Для представителей службы технической поддержки необходим специальный удобный интерфейс с набором соответствующих функций. А все поступившие по телефону заявки должны попасть в единую базу данных заявок и пройти единый цикл обработки. Упущение этих факторов может привести к серьезным недочетам и проблемам. Кроме того, они ни откуда не следуют напрямую и поэтому нуждаются в особых начальных вершинах в дереве требований – то есть мы решили, что целесообразно поместить их на главную диаграмму случаев использования.
Отметим еще одну интересную деталь. Клиент магазина не является пользователем данного ПО. Он оказывается бизнес-пользователем всей системы в целом (включая соответствующий бизнес-процесс и оборудование). На рис. 8.2 представлена бизнес-диаграмма случаев использования.
Рис. 8.2.Пример диаграммы бизнес-случаев использования
Ее можно рисовать отдельно (классики на этом настаивают), но можно пририсовывать клиента и на общую диаграмму, связав стрелкой c оператором. Часто бывает, что востребована не очень концептуальная, но компактная запись.
Каждый случай использования сопровождается небольшим текстовым описанием, а в дальнейшем может содержать целые главы в техническом задании. Диаграммы случаев использования могут служить структурой технического задания или его отдельных частей.
Другие версии. На практике диаграммы случаев использования создаются не только таким способом, как указано выше. Многие практики предпочитают строить очень детальные модели, прорисовывая на них все небольшие случаи использования, а также многочисленные связи между ними (использование, расширение и т.д.). Кто-то решительно протестует против включения в актеры системы, взаимодействующие с данной. Другие считают неприемлемым совмещать обычные диаграммы и бизнес-диаграммы случаев использования и так далее. Какую именно вы изберете стратегию в конкретном случае, какую точку зрения поставите во главу угла – вам решать самим. Рецепта здесь нет.
Важно лишь отметить, что хорошо определенная точка зрения нужна. Она позволяет четко сфокусироваться, решать определенные, хорошо осознаваемые задачи. А также такую точку зрения можно кое-где осознанно, в угоду практической полезности, нарушать.
Случаи использования в управлении разработкой. Итак, выше мы показали, как диаграммы случаев использования могут быть полезны при выявлении первичной формализации требований. Но они могут оказаться полезными и после того, как этот процесс завершен. Результирующие диаграммы случаев использования можно применять при управлении разработкой. Менеджер проекта может отслеживать прогресс проекта по тому, сколько реализовано функциональности, необходимой пользователю. Разработчики могут иметь диаграммы случаев использования где-то перед глазами, чтобы не забывать об основной цели разработки. Эти же диаграммы могут использоваться в рабочих встречах по проекту.
Казалось бы, что может быть проще — реализовать набор функций, необходимых пользователю. Однако на деле программный проект может незаметно потерять эту цель. Вместо этого можно, например, очень долго заниматься разработкой сложной и многофункциональной архитектуры, после реализации которой разработчики обещают, что все пользовательские функции получатся почти сразу же и очень легко. Однако, как правило, оказывается, что это "сразу же" было сильным преувеличением и проект весьма выбивается из расписания, а многие заказанные пользователем функции в этом окружении сделать тяжело или невозможно. Бывает, что чрезмерная ориентация на "внутреннее совершенство" ПО оканчивается для проекта либо крупными неприятностями, либо полным крахом. Однако бывают и другие случаи, когда только такая ориентация впоследствии и спасает проект. Последнее случается, когда система долго развивается и сопровождается, или когда требования к ней внезапно и сильно меняются, или когда на ее основе делаются другие системы. Необходим баланс между внутренним совершенством программного обеспечения и функциональностью, нужной для заказчика и доставленной ему в срок. Разработка ПО в терминах случаев использования — хороший способ контролировать, что процесс создания системы двигается в нужном направлении.