Средства тестирования приложений для разработчиков.Новичков Александр
C развитием аппаратной базы компьютерных систем - увеличением тактовой частоты до запредельных уровней (переваливших за гигагерц), ускорением обработки изображений в реальном масштабе времени при помощи супермощных видеоадаптеров многие разработчики не считают нужным (или возможным) оптимизировать написанные ими программные продукты, перенося весь неоптимизированный код на быструю подсистему, быстрый процессор, "умный" компилятор. Результат подобного злоупотребления мы наблюдаем ежедневно во время запуска программ на собственных компьютерах, отмечая странную тенденцию: чем новее программа, тем больше требует ресурсов, и тем медленнее работает. Но и это еще не все! Многие программы по окончании работы не освобождают все занимаемые ресурсы, что приводит к достаточно неприятным последствиям. Странно, не правда ли? Казалось бы, технологии программирования должны совершенствоваться и идти в ногу с аппаратными новинками, качественно используя все предоставляемые ими возможности, однако на деле все обстоит гораздо хуже. В погоне за новыми цифрами версий на коробках продуктов разработчики не считают нужным ( возможным ) проводить детальную оптимизацию написанного кода, тщательно отслеживая все вызовы и подсчитывая занимаемую системную память, поскольку занятие это трудоемкое и длительное, а получаемый результат не всегда оправдывает надежды: времени потрачено много, сил - еще больше, а производительность конечного продукта повысилась в лучшем случае на 9%, а то и меньше. Согласитесь, ситуация для нашего времени достаточно типичная, причем типична она для всех софтверных компаний, вне зависимости от ранга, размера, и, что немаловажно, от географического расположения. Лозунг: "время - деньги", применяемый всеми к месту и не к месту, в данной ситуации дает явный сбой! Получается количество версий - в ущерб качеству. За примером далеко ходить не придется: у всех есть нерасторопные операционные системы и офисные пакеты, которые с каждой новой реализации повышают аппаратные требования, функционально оставаясь при этом на уровне 95 года. Неужели все так страшно и бесперспективно? И "да" и "нет". Все крупные компании, связанные с разработкой программного обеспечения, пытаются найти способы наиболее эффективного тестирования, не выходя при этом за рамки бюджета конкретной разработки. Но, к сожалению, то ли ищут не там, то ли применяют не то, поскольку оптимизированных продуктов на рынке не так много. Разработка качественных продуктов в отведенные сроки- вот задача, на решения которой направлены все продукты компании Rational. В частности, для этапа разработки высококачественного кода, посредством RUP (Rational Unified Process - методологии разработки), компанией рекомендуется использование инструментов тестирования: Quantify, Purify, PureCoverage. Именно методике использования вышеперечисленных продуктов и посвящена данная статья - ведь их разумное применение позволит группам разработчиков найти и своевременно устранить узкие места в производительности создаваемого программного обеспечения.
II. Решения… Любой разработчик рано или поздно сталкивается с проблемой необходимости увеличения производительности собственного приложения, в сжатые сроки и с максимально возможной эффективностью. Проблема достаточно нетривиальна сама по себе, поскольку, не имея необходимого инструментария, вручную придется замерять скоростные характеристики разрабатываемых приложений, дописывая для этого килобайты отладочных функций и циклов вызова. А если недосуг набивать горы новых тест-функций, придется обзавестись таймером, замеряя фрагменты кода с точностью до секунды. Естественно, что при такой "продуктивной" работе не может быть и речи о каком бы то ни было приемлемом качественном уровне создаваемого программного продукта, поскольку составление списка вызовов функций превращается в рутину, отрицательно сказывающуюся на скорости разработки. Такой уровень автоматизации можно сравнить с каменным веком - но к сожалению, подобная практика все еще имеет место быть, и никто ее не отменял. Rational Quantify - позволяет решить описанные выше проблемы, как говорится, одним махом. Это простое, но в то же время мощное и гибкое средство учета производительности приложений - незаменимый инструмент для сбора и анализа информации о производительности созданного программного продукта. Quantify генерирует в табличной форме список всех вызываемых в процессе работы приложения функций, указывая временные характеристики каждой из них. Quantify предоставляет полную статистическую выкладку, невзирая на размеры тестируемого приложения и время тестирования. Сбор данных осуществляется посредством технологии OCI (Object Code Insertion). Суть способа состоит в подсчете всех машинных циклов путем вставки счетчиков в код для каждого функционального блока тестируемой программы (все циклы приложения просчитываются реально, а не при помощи произвольных выборок, как в большинстве пакетов тестирования). Уникальность данного подхода заключается в том, что, во-первых, тестируется не только сам исходный код, но и все используемые компоненты, (например: библиотеки DLL, системные вызовы), а во-вторых, для подобного анализа совсем необязательно иметь исходные тексты тестируемого приложения. Последнюю функцию я хотел бы выделить особо, поскольку, пользуясь Quantify, любой разработчик может подвергнуть тестированию произвольное приложение, написанное в любой среде, с использованием любого известного компилятора! Гибкая система фильтров Quantify позволяет, не загромождая экран лишними выводами (например, системными вызовами), получать необходимую информацию только о внутренних, программных вызовах. Вооружившись полученной статистикой, разработчик без труда выявит узкие места в производительности тестируемого приложения и устранит их в кратчайшие сроки. Попробуем рассмотреть работу Quantify на конкретном примере - например, используя стандартную программу "Clock" из состава Windows NT 4.0 выяснить, сколько и чего она вызывает. После загрузки необходимо под контролем Quantify исполнить "Clock", как в обыкновенном отладчике, с той лишь разницей, что здесь нельзя наблюдать дизассемблированный код программы. На рисунке 1 показан диалог запуска "подопытного" приложения. Как видно, в данном диалоге нет никакой сложности. Quantify для работы необходимо знать только месторасположение программы и рабочий каталог. Рисунок 2 выводит на экран сведения о собираемой статистике. В зависимости от количества подключаемых библиотек, количество строк также изменяется. Подобная информация собирается для всех подключаемых модулей. К сожалению, "Clock" не имеет большого числа явно загружаемых модулей (точнее - ни одного), в отличие от программы "Paint", изображенной на рисунке 3. Подобным образом Quantify собирает начальную, предварительную статистику о работе приложения. После такой преамбулы запускается само тестируемое приложение, без каких бы то ни было видимых ограничений: все идет как обычно. Однако, работа Quantify на этом не заканчивается: программа продолжает контролировать тестируемое приложение, отслеживая и подсчитывая число обращений ко всем функциям, вызываемых "подопытной" программой. В любой момент времени можно получить слепок состояния (snapshot), показывающий общий список вызовов на данный момент работы, который впоследствии можно проанализировать. Вся статистика во время работы приложения отображается в специальном окне (рис. 4), где в наглядной форме демонстрируется временная диаграмма исполнения приложения. Здесь отображается временная характеристика (сколько приложение работало), демонстрируется (разноцветными квадратами), чем оно было занято в определенный момент времени и, самое важное, как завершилось. В закладках можно найти более детализованное описание как тестируемой программы, так и компьютера в целом. Еще один любопытный момент: во время сбора разработчик может работать со своим продуктом как обычно: нажимать на кнопки, заходить в меню и пр. Естественно, при этом вызываются явным образом определенные функции в тестируемой программе, которые тут же попадают в статистику Quantify. Прервать процесс возможно в любой момент, закрыв тестируемое приложение (обратите внимание на рисунок 4. В конце диаграммы отображается черный квадрат, говорящий о том, что приложение завершилось). Здесь завершается механический этап сбора статистики и начинается творческий анализ. Способов, предоставляемых Quantify для анализа, более чем достаточно: можно посмотреть и дерево вызовов (Call Graph), и список функций с точными замерами (Function List), а если Вы разрабатываете свои приложения в среде Visual Studio, то Quantify позволит вызывать исходные тексты тестируемых функций (в этом случае эффективность разработки повышается за счет интеграции Quantify с Visual Studio). Начнем описание по порядку. На рисунке 5 показано дерево вызовов для приложения "Clock". Двойное нажатие на любой функции вызовет либо поддерево (если функция разворачиваемая), либо статистическую выкладку по числу вызовов данной функции. Следующий рисунок (№ 6) демонстрирует окно списка, где отображаются в табличном виде наименования всех функций со всеми характеристиками. Здесь можно, отсортировав список по определенному признаку, выяснить какие функции сколько раз вызывались и сколько времени заняли. Табличное представление позволяет сравнивать временные интервалы и вызовы нескольких функций (вызовов) для более глубокого сравнительного анализа. Возвращаясь к рисунку 6, хочется обратить ваше внимание на то, какую детальную информацию представляет Quantify. Приглядевшись, можно заметить, что самая часто вызываемая функция это - "GetLocalTime", число вызовов составляет 5831. Соответственно, получив такую информацию, разработчик сам решит, стоит или нет заниматься ее оптимизацией. Рисунок 7 демонстрирует следующий уровень детализации статистики вызовов. Здесь выводится та же информация, что и на предыдущем шаге, но уже только для конкретной функции и с использованием более понятных в визуальном отношении гистограмм. К сожалению, объем статьи не позволяет более детально объяснить все аспекты применения Rational Quantify. Но это и не обязательно, так как хочется вызвать интерес как можно большего числа людей к программным продуктам компании Rational, которые пока еще недостаточно хорошо знакомы большинству российских разработчиков. В заключение коротко рассмотрим возможности программы Quantify:
Начать описание возможностей продукта Rational Purify хочется перефразированием одного очень известного изречения: "с точностью до миллиБАЙТА". Данное сравнение не случайно, ведь именно этот продукт направлен на разрешение всех проблем, связанных с утечками памяти. Ни для кого не секрет, что многие программные продукты ведут себя "не слишком скромно", замыкая на себя во время работы все системные ресурсы без большой на то необходимости. Подобная ситуация может возникнуть вследствие нежелания программистов доводить созданный код "до ума", но чаще подобное происходит не из-за лени, а из-за невнимательности. Это понятно - современные темпы разработки ПО в условиях жесточайшего прессинга со стороны конкурентов не позволяют уделять слишком много времени оптимизации кода, ведь для этого необходимы и высокая квалификация, и наличие достаточного количества ресурсов проектного времени. Как мне видится, имея в своем распоряжении надежный инструмент, который бы сам в процессе работы над проектом указывал на все черные дыры в использовании памяти, разработчики начали бы его повсеместное внедрение, повысив надежность создаваемого ПО В общих чертах работа программы сводится к детальному выводу статистики об использовании памяти приложением. Данных в выводимой статистике вполне достаточно для получения общей, а затем - и детальной информации обо всем, что имеет отношение к памяти: утечки, потерянные блоки, фиктивные ссылки. Purify позволяет анализировать исполняемый модуль, содержащий отладочную информацию, либо работать на уровне исходников, но только в среде Visual C++. Работа программы традиционно начинается со сбора информации о загружаемых библиотеках, любых внешних модулях. Так же, как и в Quantify, сначала идет детальный сбор информации и анализ загружаемых библиотек, затем анализ плавно перетекает на исполняемый модуль. В зависимости от производительности и количества оперативной памяти данный процесс может очень надолго затянуться… Поскольку Purify тесно интегрируется с Microsoft Development Studio, мне хочется построить дальнейшее изложение материала в перспективе интеграции двух продуктов. Интеграция компонентов от Rational выражается в появлении новых инструментальных панелей на поверхности рабочего стола в Development Studio. Получив полный набор тестирующих и профилирующих средств, разработчик обращается к ним по мере необходимости, не покидая рабочего пространства, что позволяет сэкономить массу времени на различные вызовы сторонних программ. Рисунок 8 показывает примерный вид инструментальных панелей в Visual Studio, появляющихся после инсталляции Purify, Quantify, PureCoverage. А учитывая давнюю дружбу Rational и Microsoft, становится понятно, почему поддерживаются в полной мере только среды разработчиков от MS. Собрав воедино всю перечисленную информацию, давайте попробуем на конкретном примере в Visual Studio, создать приложение, внести в него ряд намеренных ошибок после чего, используя Purify, отыскать их. Опустим за скобки то, каким образом создаются проекты в Visual Studio и из чего они состоят - это не самоцель данной статьи. Считаем, что читатель уже работал с данной средой, или хотя бы знаком с ней - это - во-первых. Во-вторых, попробуем внести некоторый зловредный код на С++ (в стандартный шаблон, генерируемый Visual Studio при использовании "мастера"), с преднамеренным вызовом функции выделения блока памяти. Для детального тестирования нам необходимо будет дважды исполнить одну и ту же программу: в одном случае со "зловредным" кодом, а другой раз без него (в качестве "зловредного" кода я подразумеваю безвозвратное выделение блока памяти), а в другом: все тоже самое, только с возвратом блока системе) Итак, для дальнейшей демонстрации возможностей я подготовил проект при помощи "волшебника", создающего 32-разрядное приложение для Windows. Имя проекту дал "PROJECTOID". На рисунке 9 изображен скриншот окна Workspace после создания проекта. Для демонстрации преимуществ Purify не нужно заводить в примере тонны сложных классов, запутывая и себя, и программу, и статью: ограничимся лишь простыми вызовами на распределение памяти Для более наглядного способа отлова ошибок допишем пару строк в стандартный обработчик "OnAppAbout": void All::OnAppAbout() { char *alex; //Наша строка №1 alex=(char *)malloc(20000); //Наша строка №2 CAboutDlg aboutDlg; aboutDlg.DoModal(); } Добавление интеллекта к функции OnAppAbout сделано намеренно, поскольку во время работы можно воспользоваться данной функцией несколько раз подряд, активируя диалог "ABOUT" после игр с его вызовом. Теперь завершим приложение, посмотрим статистику по памяти и под конец найдем "виновного" в полученной утечке памяти. Из фрагмента видно, что указатель "alex" смотрит в сторону блока длиной в 20Кб, который выделяется функцией MALLOC. Еще можно заметить, что:
Запускаем программу по F5, предварительно активировав Purify (увеличительные стекла на инструментальной панели Purify. См. рис. 8). В запущенном приложении трижды запускаем диалог ABOUT из верхнего меню и закрываем приложение. Во время работы подконтрольного приложения Purify собрала достаточно информации о нем и о его проблемах с памятью. Полученная статистика выведется на экран сразу по окончании работы приложения. Рисунок 10 иллюстрирует вид окна со статистикой по памяти. При внимательном рассмотрении становится видна вся подноготная как нашего модуля, так и шапки, сгенерированной компилятором Microsoft в лице Visual C++. Purify насчитала 43 (!) предупреждения о неправильном (читай: неэффективном) использовании памяти, а из них только одно наше было преднамеренно введено в программу. Что же, теперь понятно, почему все продукты от MS так охочи до памяти! Вновь обратимся к рисунку со статистикой, где в явном виде находится информация по ошибкам и по модулям, в которых эти ошибки были обнаружены. К приятной неожиданности можно отнести фразу "Memory leak of 60000", указывающую на то, сколько фактических байтов памяти программа не вернула системе по завершении своей работы. Эта черта разительно отличает подходы к тестированию программы Rational Purify от подобных продуктов конкурирующих компаний, которые высчитывают не фактические утечки (полученные в результате нескольких вызовов при реальной работе приложения, а количество невозвращенных блоков, то есть ограничиваются лишь анализом (на уровне исходных текстов) программы с выявлением вызовов функций аллокирования памяти без последующего освобождения. Из этого и следует полученное число 60000 - фактически не освобожденных блоков (3 по 20000). После добавления функции free(alex) в конец обработчика OnAppAbout и перекомпиляции тестируемого приложения, Purify не обнаруживает никаких ошибок памяти, что и являлось нашей целью. Все вышеописанные возможности дают мощный инструментарий в руки разработчика, желающего знать, где в коде находится вызов на выделение памяти, сколько физически утеряно блоков в результате работы приложения и какая часть программы в этом виновата. Еще очень важным моментом работы Purify является возможность включения опции остановки на ошибке в подопытном приложении: так, если в приложении есть Run-time-ошибка, то повстречавшись с ней оно (приложение), не "вылетит" по дампу и не позовет "доктора Ватсона", а просто завершится с передачей управления Purify, который и сообщит программисту о том, где произошел сбой, по каким причинам и сколько при этом было использовано памяти. Описанные возможности продукта Rational Purify - основные, но не единственные. Просто рамки статьи не позволяют сконцентрироваться на всех мелочах и тонкостях, присущих данному продукту, что, впрочем, и неважно, так как для этого есть руководство пользователя, где по пунктам расписаны все супервозможности продукта. По сложившейся традиции, подведем итоги статьи описанием основных возможностей Purify:
Все хорошее рано или поздно кончается. Хотя, исходя из личного опыта и опыта знакомых - все самое хорошее заканчивается гораздо раньше, чем того хотелось. Вот так и с продуктами тестирования, рассматриваемыми в данной статье: мы сначала уделили достаточно много сил и времени продукту Quantify, для получения представления о нем и о принципах тестирования по Rational’овски, затем углубились в область тестирования утечек памяти, поговорив о Purify в контексте интеграции с Development Studio. Теперь настало время поговорить еще об одном продукте - Rational Pure Coverage , завершающим вереницу инструментов по тестированию. Он позволит разработчикам довести собственные программы до состояния абсолютной эффективности (божественной нирваны), освободив от ошибок и странностей. Основное и единственное назначение продукта - выявление участков кода, пропущенного при тестировании приложения. Вполне очевидно, что при тестировании программы специалисту не удается оттестировать абсолютно все ее функции. Это невозможно по двум причинам: во-первых, разработчик не может сделать все абсолютно правильно с учетом всех возможных нюансов, во-вторых, даже учитывая все возможные реакции приложения на внешние "раздражители" невозможно на 100% быть уверенным в том, что все оттестировано. Согласитесь - достаточно обидно будет лицезреть на демонстрации собственной программы перед коллегами или (о, ужас!) клиентами сообщение об ошибке. После такого провала уже никому не доказать, сколько бессонных ночей проведено в детальном всестороннем тестировании программы, и что как раз именно не прошедшая тестирование функция привела к фатальному сбою... Но если Вы, как разработчик, воспользуетесь средством Rational PureCoverage, то сможете раз и навсегда забыть о мучительном поиске невыполненного кода в собственной программе. Pure Coverage собирает статистику о тех участках программы, которые во время тестирования не были выполнены (пройдены). Подобные строки Pure Coverage подсвечивает красным цветом, четко указывая на наличие черных дыр в программе в виде неоттестированного кода, и тем самым давая разработчику хорошую пищу для размышлений. В работе данный инструмент так же прост, как и предыдущие. Вызывать его можно как из Visual Studio, так и независимым приложением, например, из командной строки. По принципу работы Pure Coverage слегка напоминает Quantify, поскольку также подсчитывает количество вызовов функций. Правда, получаемая статистика не столь исчерпывающая как в Quantify, но для целей проверки отдельных частей на предмет прохождения теста вполне пригодна. На рисунке 13 показана статистическая выкладка, собранная Coverage. Как можно видеть, программа способна предоставить отчет пользователю в двух уровнях детальности: на уровне модулей и на уровне файлов - каждый из них показывает практически одно и тоже, но как бы "с разных сторон". Статистика, показанная справа от имени функции, описывает число вызовов, ее статус, а также дополнительную информацию по выполненным линиям в исходном тексте: "Lines Hits" - число строк в процедуре, "Lines Missed" - пропущено при тестировании. Анализ только данного окна статистики позволит и отыскать не оттестированные функции, и перейти к исходному тексту для детального рассмотрения. В целях демонстрации я внес некоторые изменения в диалог OnAppAbout, описанный выше, а для усиления эффекта - блок проверки значения указателя "alex". Как известно, функция malloc возвращает 0 при невозможности выделения блока памяти запрошенного размера. Соответственно, при совпадении условия (нехватка памяти!) на экран выведется диалоговое окно с устрашающим предупреждением об ошибке. void All::OnAppAbout() { char *alex; alex=(char *)malloc(20000); if(!alex)MessageBox(0,"ERROR","ERROR",MB_OK); CAboutDlg aboutDlg; aboutDlg.DoModal(); free(alex); } В результате изменений, внесенных в тело функции, мне пришлось перекомпилировать приложение и вновь воспользоваться тройным входом в диалог About (сообщений о нехватке памяти получено не было). Последний раз вернемся к 13-й иллюстрации, где точно показано число вызовов и число потерянных строк. Находясь в данном окне, переходим на уровень просмотра исходных текстов ("Source Annotace"). На 14 рисунке виден текст функции OnAppAbout, отдельные части которого подсвечиваются разными цветами в зависимости от того, прошла функция "тестирование" или нет. Пункт "Line Coverage" выводит точное число выполнений конкретной строки во время тестирования приложения, а не функции в целом. Для удобства восприятия информации все линии автоматически нумеруются, а цвета подсветки настраиваются пользователем по собственному усмотрению. И еще один подарок от разработчиков: все данные, полученные в результате работы программы можно импортировать либо в Word, либо в Excel. m Вернемся к вопросу о хорошем, добром и вечном, но грустном: ведь на описании продукта Coverage придется закончить наш разговор о средствах тестирования для разработчиков, предоставляемых компанией Rational Software. Описанные три продукта полностью покрывают весь спектр задач, связанных с разносторонним тестированием программного обеспечения на стадии его разработки и помогают программистам строить более качественное программное обеспечение в реальные сроки, что немаловажно в условиях бурно развивающейся отрасли, каковой являются Информационные Технологии. Очень радует тот момент, что компания Rational постоянно держит "руку на пульсе", улавливая все, даже самые малейшие колебания рынка разработки ПО, предлагая все новые и новые средства решения возникающих проблем. |