Содержание


Практическое использование Rails

Часть 4. Стратегии тестирования в Ruby on Rails

Выбор оптимальных инструментальных средств и технологий для команды Rails-програмистов

Comments

Серия контента:

Этот контент является частью # из серии # статей: Практическое использование Rails

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Практическое использование Rails

Следите за выходом новых статей этой серии.

Основной отличительной особенностью платформы 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: Тест основной модели
require File.dirname(__FILE__) + '/../test_helper'

class BannerStyleTest < Test::Unit::TestCase
  fixtures :banner_styles

  def test_associations
    assert_working_associations
  end

  def test_validation_with_incorrect_specs_should_fail
    bs =  BannerStyle.new(:height => 10, :width => 10, :format => 'vertical_rectangle',
                          :model_name => 'Nonprofit')
    assert !bs.save, bs.errors.inspect

    bs2 =  BannerStyle.new(:height => 400, :width => 240, :format => 'vertical_rectangle',
                          :model_name => 'Nonprofit')
    assert bs2.save, bs2.errors.inspect
  end

  ...
end

В листинге 1 показан небольшой набор тестовых данных с двумя тестами. Подпрограмма BannerStyle создает простые рекламные баннеры. Размеры и формы каждого баннера зависят от стандартов. Чтобы обеспечить соответствие каждого нового баннера необходимым стандартам, в приложении используется таблица стандартов. В первом тесте helper-метод используется для проверки всех ассоциаций в BannerStyle посредством отражения, как показано в листинге 2. Второй тест не позволяет сохранять баннеры с некорректными значениями высоты и ширины и обеспечивает правильное сохранение моделью баннеров с верными характеристиками.

Листинг 2. Helper-метод для тестирования рабочих ассоциаций
def assert_working_associations(m=nil)
  m ||= self.class.to_s.sub(/Test$/, '').constantize
  @m = m.new
  m.reflect_on_all_associations.each do |assoc|
    assert_nothing_raised("#{assoc.name} caused an error") do
      @m.send(assoc.name, true)
    end
  end
  true
end

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

Функциональное и комплексное тестирование

Функциональные тесты проверяют работу пользовательского интерфейса при помощи отдельных HTTP-запросов. Среда Rails позволяет легко инициировать отдельные команды HTTP GET и POST, формируя основу тестов. Комплексные тесты аналогичны функциональным тестам, но они позволяют инициировать множество каскадных HTTP-запросов. Принцип и структура тестов те же самые. В листинге 3 показано несколько простых функциональных тестов.

Листинг 3. Простой функциональный тест
require File.dirname(__FILE__) + '/../test_helper'
require 'causes_controller'

class CausesController; def rescue_action(e) raise e end; end

class CausesControllerTest < Test::Unit::TestCase
  fixtures :causes, :members, :quotes, :cause_images, :blogs, :blog_memberships

  def setup
    @controller = CausesController.new
    @request    = ActionController::TestRequest.new
    @response   = ActionController::TestResponse.new
  end

  def test_index
    get :index

    assert_response :success
    assert_template 'index'

    assert_not_nil assigns(:causes)
    assert_equal Cause.find_all_ordered.size, assigns(:causes).size
  end

  def test_should_create_blog
    assert Cause.find(2).blog.nil?
    get :create_blog, :id => 2
    assert Cause.find(2).blog.nil?

    login_as :bruce
    get :create_blog, :id => 2
    assert !Cause.find(2).blog.nil?
    assert_equal Cause.find(2).name, Cause.find(2).blog.title
  end

Из листинга 3 видно, что все взаимодействия между тестом и системой выполняются посредством HTTP-методов GET и POST. Основной алгоритм тестирования выглядит следующим образом:

  1. Запуск простой HTTP-операции.
  2. Проверка воздействия HTTP-операции на систему.

Более того, в листинге 3 метод setup задает фиксированные значения для моделирования HTTP-запросов. С их помощью устраняются все требования к сети и инфраструктуре, тем самым наборы тестовых данных изолируются рамками самого приложения.

Заглушки

В проекте ChangingThePresent.org мы добавили несколько тестовых helper-методов, которые упростили, к примеру, процедуру регистрации в системе. В листинге 3, в пятой строке метода test_should_create_blog показан вызов метода login_as :bruce. Данный код запускает helper-метод, показанный в листинге 4, который заменяет процедуру регистрации входа на сайт, копируя ID пользователя непосредственно в сессию. Если вы использовали плагин Rails acts_as_authenticated, то знаете, что вошедшему в систему пользователю присваивается значение, связанное с ключом :user сессии.

Листинг 4. Заглушка для входа в систему
def login_as(member)
  @request.session[:user] = member ? members(member).id : nil
end

Большинство разработчиков путают понятия заглушка (stub) и фиктивный объект (mock). Заглушка просто заменяет реальное воплощение его упрощенной реализацией. В листинге 4 заглушка заменила всю систему регистрации на её упрощенную имитацию. Заглушка имитирует реальное положение вещей. Mock-объкты не являются заглушками. Mock-объект похож на датчик, который измеряет, каким образом приложение использует интерфейс. Далее будет приведено более детальное описание заглушек и показано несколько примеров.

Основные понятия

Теперь вы знаете, как использовать встроенные средства тестирования Rails. Прежде чем идти далее, хотелось бы обозначить пару ключевых проблем: объём и скорость тестирования. Раз вы вырабатываете общую философию тестирования, вам потребуется обратить внимание на нахождение компромисса между скоростью и покрытием теста.

Тестовое покрытие

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

В проекте ChangingThePresent для определения тестового покрытия используется RCov. Можно запустить стандартную команду rake и в качестве отчета получить обычный набор точек. Также можно запустить rake test:coverage для получения более полного и подробного отчета, например такого, который показан в листинге 5.

Листинг 5. Запуск rake test:coverage с RCov
807 tests, 2989 assertions, 0 failures, 0 errors
+----------------------------------------------------+-------+-------+--------+
|                  File                              | Lines |  LOC  |  COV   |
+----------------------------------------------------+-------+-------+--------+
|app/controllers/address_book_controller.rb          |   142 |   123 |  84.6% |
|app/controllers/admin_controller.rb                 |    77 |    65 |  93.8% |
|app/controllers/advisor_admin_controller.rb         |    86 |    63 |  88.9% |
|app/controllers/advisors_controller.rb              |    52 |    42 | 100.0% |

...


|app/models/stupid_gift.rb                           |    56 |    45 | 100.0% |
|app/models/stupid_gift_image.rb                     |    10 |    10 | 100.0% |
|app/models/titled_comment.rb                        |     2 |     2 | 100.0% |
|app/models/upgrade.rb                               |    13 |    10 | 100.0% |
|app/models/upgrade_item.rb                          |     3 |     3 | 100.0% |
|app/models/validation_model.rb                      |     7 |     7 | 100.0% |
|app/models/volunteer_opportunity.rb                 |   137 |   129 |  93.0% |
|app/models/work_period.rb                           |     5 |     4 | 100.0% |
+----------------------------------------------------+-------+-------+--------+
|Total                                               | 12044 | 10044 |  81.8% |
+----------------------------------------------------+-------+-------+--------+
81.8%   167 file(s)   12044 Lines   10044 LOC

Выполнение RCov занимает гораздо больше времени (примерно в два раза по сравнению с временем, необходимыми на ведение протокола наших тестов), поэтому я не пользуюсь этой командой постоянно. Однако запустив её, можно будет точно узнать значение тестового покрытия в любом файле. И, что еще лучше, можно открыть файл покрытия в окне браузера и посмотреть, какие строки кода уже охвачены тестами. На рисунке 1 показан пример типичного отчета о покрытии.

Рисунок 1. Реальный отчет RCov
Рисунок 1. Реальный отчет RCov
Рисунок 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 показан код, который будет заставлять системный класс Date всегда возвращать одну и ту же дату, например, День сурка.

Листинг 6. Создание простой заглушки
ground_hog_day = Date.new(2007, 2, 2)
Date.stubs(:today).returns(ground_hog_day)
assert_equal 2, Date.today.day

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

В Rails присутствует, по меньшей мере, три библиотеки фиктивных объектов: Mocha, FlexMock и RSpec. Более подробно я расскажу о Mocha, однако у каждой библиотеки есть свои достоинства. При использовании библиотеки Mocha фактически происходит перечисление каждого ожидаемого обращения к API с последующим указанием результатов, которые Mocha должна вернуть, как показано в листинге 7.

Листинг 7. Библиотека фиктивных объектов Mocha
mock_member = mock("member_67")
mock_member.expects(:valid?).returns(true)
mock_member.expects(:save).returns(true)
mock_member.expects(:valid_captcha=).with(true)
mock_member.expects(:plaintext_password).returns('password')
mock_member.expects(:id).returns(67)
Member.expects(:find_by_login).with(nil).returns(mock_member)

post :create, :member => {}, :nonprofit => {:id => 67}

...

assert_response :redirect
assert_redirected_to :controller => 'nonprofits', :action => 'show',
 :id => mock_nonprofit_id

В листинге 7 показан пример набора тестовых данных для создания нового участника. Можно задать ожидания для каждого взаимодействия между контроллером и пользователем-заглушкой. Создание участника-заглушки (mock member) и определение взаимодействий не зависят друг от друга. Затем я фактически создам заглушку для класса Member и заглушку для определителя, возвращающего mock_member.

Очевидно, что в некоторой степени имеет место взаимодействие с уровнем модели, однако mock-объект полностью изолирует поведение участника от набора данных функционального теста. В данном случае есть пара очевидных преимуществ. Использовать некоторые API, например, проверки кредитной карты или "кнопки самоликвидации", нецелесообразно. Другие API, например, службы на основе времени или памяти, недостаточно предсказуемы. Чтобы заменить их заглушками или mock-объектами придется практически всегда использовать среды mock-объектов.

Больший интерес представляет вопрос, следует ли использовать заглушки и фиктивные объекты для модели, основанной на базе данных. Одним из преимуществ является скорость: данный набор тестовых данных никак не отразится на базе данных. Еще одно преимущество - независимость. Я полностью изолировал тестируемый код от уровня контроллера. Но, возможно, вы также обнаружите и недостатки. Наборы тестовых данных были значительно усложнены. Кроме того, изменение поведения моих моделей вызывает цепную реакцию, так как необходимо изменить объект модели и наборы тестовых данных, которые их окружают. Если упущено что-либо важное, внести изменения в набор тестовых данных не составляет никакого труда. Добавление одной единственной валидации может нарушить весь сценарий и остаться незамеченным. Поэтому в проекте ChangingThePresent классы объектов модели не заменяются фиктивными классами. Мы ограничиваем использование фиктивных объектов только рамками внешних интерфейсов, например, Web-сервисами для третьих лиц или сетевыми сервисами.

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

Непрерывная интеграция

Наиболее важным изменением, которое мы добавили в итоговую процедуру, является непрерывная интеграция (Continuous integration - CI). Мы запустили версию Cruise Control для Ruby. Наш сервер CI проверяет правильность сборки и запускает наборы тестовых данных с нуля при каждой регистрации нового изменения. Сервер оповещает каждого разработчика всякий раз, когда изменение нарушает процесс сборки. Сервер позволяет запускать несколько репрезентативных тестов перед регистрацией. Можно изменить всего лишь несколько строк в Member, а затем запустить unit/member_test.rb и functional/members_controller_test.rb. Через пятнадцать секунд я смогу выполнить регистрацию, будучи уверенным, что Cruise Control оповестит меня, если есть какие-либо ошибки.

Заключение

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

  • Должны ли все тесты быть реализованы на основе баз данных или для локализации функциональных тестов следует использовать заглушки и фиктивные объекты?
  • Является ли 100% покрытие достижимой целью?
  • Представляет ли повышенная скорость теста с использованием БД в оперативной памяти дополнительный риск?

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


Ресурсы для скачивания


Похожие темы

  • Оригинал статьи: Real world Rails, Part 4: Testing strategies in Ruby on Rails (EN).
  • ChangingThePresent.org - благотворительный портал, который положен в основу данной серии статей. Здесь можно отдать в дар акр тропического леса, помочь вернуть зрение слепому или пожертвовать час времени на исследование рака (EN).
  • Загрузите ознакомительные версии продуктов IBM (EN).
  • Узнайте больше о непрерывной интеграции с помощью CruiseControl.rb. Сервер непрерывной интеграции ThoughtWorks сэкономил нам несчетное количество часов (EN).
  • Mock-объекты - это не заглушки: в этой статье Мартина Фаулера (Martin Fowler) рассказывается о различии между заглушками и фиктивными объектами (EN).
  • FlexMock - среда фиктивных объектов Ruby, которая в настоящее время пользуется огромной популярностью (EN).
  • Mocha - среда фиктивных объектов, используемая в проекте ChangingThePresent (EN).
  • RCov - отличный инструмент анализа покрытия кода для Ruby (EN).
  • Подпишитесь на рассылку информационного бюллетеня developerWorks по Web-разработке (EN).

Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Web-архитектура, SOA и web-сервисы
ArticleID=317664
ArticleTitle=Практическое использование Rails: Часть 4. Стратегии тестирования в Ruby on Rails
publish-date=07012008