Практическое использование Rails: Часть 4. Стратегии тестирования в Ruby on Rails (исходники)Источник: IBM developerWorks Россия Брюс Тэйт
Основной отличительной особенностью платформы Rails является сам язык Ruby. Как и любой язык программирования с динамической типизацией, Ruby обладает достаточной гибкостью, удобством использования и хорошей производительностью. Но все имеет свою цену. В языках программирования с динамической типизацией нет компилятора, который бы отслеживал определенные виды ошибок, в том числе довольно распространённые ошибки типов и орфографические ошибки. Пользователи объектно-ориентированных языков программирования с динамической типизацией очень быстро поняли, что им необходимо тестировать свои приложения. В сообществе Ruby on Rails к тестированию относятся так же, как в США - к телешоу American Idol. Члены сообщества регулярно следят за результатами тестов. Разработчики Ruby много говорят о тестировании, пишут о нём в своих блогах и даже ведут деятельность в оффлайне: конечно же, они не голосуют с мобильных телефонов, а принимают участие в создании сред с открытым исходным кодом. Если бы тестирование не выполнялось, то в Ruby-приложениях было бы гораздо больше ошибок на одну строчку кода. Тестирование позволяет использовать все преимущества языков программирования с динамической типизацией и минимизировать их недостатки. В данной статье не рассматриваются такие общие вопросы, как "необходимо ли выполнять тестирование или нет", или "как убедить руководство, что усилия, затрачиваемые на тестирование, оправданы". Будем считать, что вы уже используете тестирование. Вместо этого будут проанализированы некоторые более тонкие решения, к которым, в конечном счете, должен прибегнуть каждый руководитель проекта Ruby. Мы поговорим о том, как можно измерить тестовое покрытие и какое количество тестов необходимо выполнять. В нашей статье будет проведен подробный анализ основных методик тестирования и представлено их сравнение с новейшими mock-средами (mocking frameworks). Данная статья не является пошаговым руководством - в ней приведено несколько примеров методик тестирования, которые применялись для создания сайта ChangingThePresent.org, и у вас есть возможность увидеть их в действии. Кроме того, в статье будут показаны сильные и слабые стороны различных методик тестирования. Встроенные средства тестирования RailsВ среду Rails встроена на удивление надежная и готовая к использованию система тестирования. Затрачивая минимум усилий, можно задавать воспроизводимые настройки баз данных, отправлять Web-приложениям тестовые HTTP-сообщения и выполнять три вида тестирования: модульное, функциональное и комплексное. В следующем разделе приведены краткие примеры всех видов тестирования. Модульные тестыМодульные тесты проверяют код модели Rails и иногда helper-методы. Модульные тесты позволяют убедиться, что модель выполняет то, для чего она была создана, и что ассоциации в модели ведут себя так, как и предполагалось. Вы уже знаете, что модели Rails являются объектами, которые работают только с одной таблицей базы данных. В большинстве случаев каждый столбец базы данных является атрибутом модели. Helper-методы Rails представляют собой функции, которые помогают упростить код модели, представления или контроллера. Необходимо убедиться, что для каждой модели или helper-метода имеется тест. В проекте ChangingThePresent модульные тесты для большинства основных моделей очень небольшие. Листинг 1: Тест основной модели
В листинге 1 показан небольшой набор тестовых данных с двумя тестами. Подпрограмма BannerStyle создает простые рекламные баннеры. Размеры и формы каждого баннера зависят от стандартов. Чтобы обеспечить соответствие каждого нового баннера необходимым стандартам, в приложении используется таблица стандартов. В первом тесте helper-метод используется для проверки всех ассоциаций в
В листинге 2 показан helper-метод, который проверяет все ассоциации класса. Метод Функциональное и комплексное тестированиеФункциональные тесты проверяют работу пользовательского интерфейса при помощи отдельных HTTP-запросов. Среда Rails позволяет легко инициировать отдельные команды HTTP GET и POST, формируя основу тестов. Комплексные тесты аналогичны функциональным тестам, но они позволяют инициировать множество каскадных HTTP-запросов. Принцип и структура тестов те же самые. В листинге 3 показано несколько простых функциональных тестов. Листинг 3. Простой функциональный тест
Из листинга 3 видно, что все взаимодействия между тестом и системой выполняются посредством HTTP-методов GET и POST. Основной алгоритм тестирования выглядит следующим образом:
Более того, в листинге 3 метод setup задает фиксированные значения для моделирования HTTP-запросов. С их помощью устраняются все требования к сети и инфраструктуре, тем самым наборы тестовых данных изолируются рамками самого приложения. ЗаглушкиВ проекте ChangingThePresent.org мы добавили несколько тестовых helper-методов, которые упростили, к примеру, процедуру регистрации в системе. В листинге 3, в пятой строке метода
Большинство разработчиков путают понятия заглушка (stub) и фиктивный объект (mock). Заглушка просто заменяет реальное воплощение его упрощенной реализацией. В листинге 4 заглушка заменила всю систему регистрации на её упрощенную имитацию. Заглушка имитирует реальное положение вещей . Mock-объкты не являются заглушками. Mock-объект похож на датчик, который измеряет, каким образом приложение использует интерфейс. Далее будет приведено более детальное описание заглушек и показано несколько примеров. Основные понятияТеперь вы знаете, как использовать встроенные средства тестирования Rails. Прежде чем идти далее, хотелось бы обозначить пару ключевых проблем: объём и скорость тестирования. Раз вы вырабатываете общую философию тестирования, вам потребуется обратить внимание на нахождение компромисса между скоростью и покрытием теста. Тестовое покрытиеНа данном этапе необходимо принять одно из самых важных решений - в каком объеме необходимо проводить тестирование. Если выполнить недостаточное количество тестов, это может сказаться на качестве кода и, вероятно, даже приведёт к задержке поставки готового ПО. С другой стороны, можно выполнить и слишком большое количество тестов. Если проводить тестирование слишком долго, есть вероятность превышения сроков поставки готового ПО, что крайне нежелательно в условиях бизнеса. Чтобы принять взвешенное и обдуманное решение относительно количества выполняемых тестов, следует тщательно измерить, какой их объем уже выполняется. Одним из важнейших численных показателей, связанных с тестированием, является покрытие кода. В проекте ChangingThePresent для определения тестового покрытия используется RCov. Можно запустить стандартную команду
Выполнение RCov занимает гораздо больше времени (примерно в два раза по сравнению с временем, необходимыми на ведение протокола наших тестов), поэтому я не пользуюсь этой командой постоянно. Однако запустив её, можно будет точно узнать значение тестового покрытия в любом файле. И, что еще лучше, можно открыть файл покрытия в окне браузера и посмотреть, какие строки кода уже охвачены тестами. На рисунке 1 показан пример типичного отчета о покрытии. Рисунок 1. Реальный отчет RCovТеперь, когда у нас есть численные данные, можно начать делать приблизительные оценки необходимого объема тестов. При тестировании сайта ChangingThePresent статистические данные по тестовому покрытию варьировались, но в итоге мы пришли к значению 80 - 85%. Так как на стадии разработки находятся новые важные функции, тестовое покрытие будет временно уменьшено. Как только эти функции станут доступными пользователям по сети, тестовое покрытие увеличится. В настоящий момент значение тестового покрытия равно 81,7%. Имейте в виду, что полученные нами результаты могут в итоге отличаться от ваших. Полнота тестового покрытия зависит от опытности разработчиков, сложности приложения, критичности наличия ошибок для работы приложения и допустимости задержки сроков сдачи приложения с точки зрения бизнеса. Если разрабатывается приложение для проектирования самолетов, понадобится большее количество тестов, а если вы ради собственного удовольствия создаете приложение Web 2.0 для Facebook, которое через два месяца будет никому не нужно, если случайно не попадёт в незанятую рыночную нишу, то объем тестирования будет гораздо меньше. Лучшие из известных мне Ruby-программистов достигают покрытия выходного кода свыше 80%, а некоторые стремятся добиться 100% покрытия. Даже если удастся добиться 100% покрытия, у вас все равно не будет никаких гарантий качества самих тестов. Чтобы добиться максимально возможного тестового покрытия, необходимо выполнять различные типы тестов, моделирующих как стандартный ход выполнения операций (happy paths), так и граничные условия. Теперь, когда у вас есть инструментальные средства для оценки необходимого объема тестирования, можно перейти к обсуждению скорости тестирования. В Rails скорость тестирования определяется базой данных. Традиционные попытки проведения тестирования с помощью инструментальных средств на основе баз данных сопряжены с проблемами. Наиболее значимыми проблемами являются повторяемость и скорость. С точки зрения повторяемости, крайне сложно создать хороший набор тестов без изменения базы данных. Дело в том, что изменение базы данных приводит к изменению тестовых данных, которое в свою очередь оказывает влияние на поведение самих тестов. А вторая проблема - это скорость. Изменение базы данных - достаточно дорогое удовольствие. СкоростьКак известно, среда Rails решает проблему повторяемости посредством фиксированных величин. Каждый разработчик задает фиксированные величины для тестовых данных. Перед выполнением каждого набора тестовых данных среда тестирования Rails будет полностью стирать данные всех моделей и загружать все фиксированные величины, которые были заданы для каждого набора тестовых данных. Теперь каждый набор тестовых данных может начинаться с нулевого состояния. Однако в каждом наборе тестовых данных содержится несколько отдельных тестов, каждый из которых должен быть абсолютно независимым от всех остальных. Загрузка целого набора фиксированных величин для каждого отдельного теста будет проходить очень медленно. Rails частично решает проблему скорости, приходя к блестящему компромиссу. После выполнения каждого контрольного примера Rails откатывает все изменения, сделанные в базе данных . Откат работает гораздо быстрее, чем загрузка всех фиксированных величин с нуля. Однако затраты на доступ к базе данных очевидны. Даже с использованием откатов тестирование на основе базы данных все равно остается медленным. А если тесты работают медленно, то разработчики просто не будут их запускать. А если тесты не запускают, то они абсолютно бесполезны. Хотя Rails справилась с решением проблемы повторяемости, проблема скорости по-прежнему решена не полностью. Скорость выполнения тестирования будет оказывать влияние на развитие стратегий тестирования в ближайшие годы. Одним из альтернативных подходов является использование для тестирования базы данных, размещённой в оперативной памяти. Как правило, SQLite работает гораздо быстрее, чем MySQL. С другой стороны, тестирование может выполняться на платформе, отличной от платформы вашей производственной системы. Если для поддержки базы данных используется ActiveRecord, вероятно, что модульное тестирование будет выполняться на основе базы данных, а низкая скорость компенсируется затратами на разработку. Но ничто не заставляет использовать для функциональных тестов модели на основе баз данных. В настоящее время многие разработчики Rails используют заглушки (stubs) или фиктивные объекты (mocks), чтобы не прибегать к базе данных, создавая тем самым функциональные тесты с особо высоким быстродействием. Фиктивные объекты и заглушки в Mocha и FlexMockРанее я рассказывал, что заглушка просто заменяет реальное воплощение упрощенной реализацией. В наборах тестовых данных заглушки могут использоваться для того, чтобы упростить и ускорить реализацию и сделать её более предсказуемой. Например, может потребоваться, чтобы системные часы все время выдавали одно и то же время, чтобы иметь возможность получить повторяемые результаты теста и проверить их. Среда Mocha упрощает использование заглушек. Достаточно указать, какой результат вы бы хотели получить. В листинге 6 показан код, который будет заставлять системный класс
Если заглушка реализует упрощенную модель реального мира, то фиктивный объект (mock) делает гораздо больше. Иногда простой имитации реального мира бывает недостаточно. При выполнении тестирования бывает необходимо убедиться, что код программы корректно использует API-функции. Например, может понадобиться проверить, что СУБД-приложение установило соединение с БД, выполнило запрос и разорвало соединение; или проверить, что контроллер действительно вызывает метод В Rails присутствует, по меньшей мере, три библиотеки фиктивных объектов: Mocha, FlexMock и RSpec. Более подробно я расскажу о Mocha, однако у каждой библиотеки есть свои достоинства. При использовании библиотеки Mocha фактически происходит перечисление каждого ожидаемого обращения к API с последующим указанием результатов, которые Mocha должна вернуть, как показано в листинге 7. Листинг 7. Библиотека фиктивных объектов Mocha
В листинге 7 показан пример набора тестовых данных для создания нового участника. Можно задать ожидания для каждого взаимодействия между контроллером и пользователем-заглушкой. Создание участника-заглушки (mock member) и определение взаимодействий не зависят друг от друга. Затем я фактически создам заглушку для класса Member и заглушку для определителя, возвращающего Очевидно, что в некоторой степени имеет место взаимодействие с уровнем модели, однако mock-объект полностью изолирует поведение участника от набора данных функционального теста. В данном случае есть пара очевидных преимуществ. Использовать некоторые API, например, проверки кредитной карты или "кнопки самоликвидации", нецелесообразно. Другие API, например, службы на основе времени или памяти, недостаточно предсказуемы. Чтобы заменить их заглушками или mock-объектами придется практически всегда использовать среды mock-объектов. Больший интерес представляет вопрос, следует ли использовать заглушки и фиктивные объекты для модели, основанной на базе данных. Одним из преимуществ является скорость: данный набор тестовых данных никак не отразится на базе данных. Еще одно преимущество - независимость. Я полностью изолировал тестируемый код от уровня контроллера. Но, возможно, вы также обнаружите и недостатки. Наборы тестовых данных были значительно усложнены. Кроме того, изменение поведения моих моделей вызывает цепную реакцию, так как необходимо изменить объект модели и наборы тестовых данных, которые их окружают. Если упущено что-либо важное, внести изменения в набор тестовых данных не составляет никакого труда. Добавление одной единственной валидации может нарушить весь сценарий и остаться незамеченным. Поэтому в проекте ChangingThePresent классы объектов модели не заменяются фиктивными классами. Мы ограничиваем использование фиктивных объектов только рамками внешних интерфейсов, например, Web-сервисами для третьих лиц или сетевыми сервисами. Необходимо подчеркнуть, что сообщество Ruby склоняется к стратегиям использования фиктивных объектов. Моя команда идёт против течения и однозначно выступает за то, чтобы продолжать использовать технологии на основе баз данных. Мы попробовали и то, и другое. Мы используем данные технологии, так как они в большей степени нам подходят и лучше сочетаются с используемым нами кодом. Непрерывная интеграцияНаиболее важным изменением, которое мы добавили в итоговую процедуру, является непрерывная интеграция (Continuous integration - CI). Мы запустили версию Cruise Control для Ruby. Наш сервер CI проверяет правильность сборки и запускает наборы тестовых данных с нуля при каждой регистрации нового изменения. Сервер оповещает каждого разработчика всякий раз, когда изменение нарушает процесс сборки. Сервер позволяет запускать несколько репрезентативных тестов перед регистрацией. Можно изменить всего лишь несколько строк в Member, а затем запустить ЗаключениеВесьма занимательные споры, которые несколько лет разворачивались вокруг тестирования, вращались вокруг следующего вопроса: хороши ли автоматизированные тесты? В настоящее время дебаты стали гораздо интереснее:
Я бы посоветовал при выборе технологий для вашей команды руководствоваться тем, что полезно вам и вашим клиентам. Не позволяйте какому-нибудь эксперту убедить вас в том, что, по вашему мнению, неверно. Как обычно, через несколько лет мы обнаружим, что на самом деле у нас не было ответов на все вопросы. Появятся новые технологии, а те, которые существуют в настоящее время, окажутся в немилости. То, что написано на бумаге, не всегда идеально отражает практическое применение Rails. |